22、搜索与浏览功能的实现与优化

搜索与浏览功能的实现与优化

1. 搜索功能测试

测试搜索页面原则上很简单,只需使用合适的查询字符串访问 /community/search ,并确保结果符合预期。但测试搜索的关键部分是测试分页功能。由于默认分页值为 10,所以需要在现有的 3 个用户基础上再创建至少 8 个用户。

1.1 创建额外用户

可以手动编写 8 个以上用户,但借助 Rails 允许在 YAML 文件中嵌入 Ruby 的特性,可自动生成额外用户。在 test/fixtures/users.yml 文件中添加以下代码:

# Create 10 users so that searches can invoke pagination.
<% (1..10).each do |i| %>
user_<%= i %>:
  id: <%= i + 3 %>
  screen_name: user_<%= i %>
  email: user_<%= i %>@example.com
  password: foobar
<% end %>

生成的用户 ID 为 <%= i + 3 %> ,避免与之前用户的 ID 冲突。

1.2 测试搜索成功

使用以下代码进行搜索功能的测试:

class CommunityControllerTest < Test::Unit::TestCase
  fixtures :users
  fixtures :specs
  fixtures :faqs

  def test_search_success
    get :search, :q => "*"
    assert_response :success
    assert_tag "p", :content => /Found 13 matches./
    assert_tag "p", :content => /Displaying users 1&ndash;10./
  end
end

运行测试命令:

ruby test/functional/community_controller_test.rb -n test_search_success

测试虽简短,但能捕捉到几个常见问题,在开发搜索功能时很有价值。

2. 开始浏览功能开发

浏览用户功能虽不如搜索通用,但因 Ferret 承担了主要搜索工作,浏览功能实际上更具挑战性。接下来将创建允许用户按年龄、性别和位置查找其他用户的页面。

2.1 构建浏览页面

2.1.1 后端动作

app/views/controllers/community_controller.rb 中定义 browse 动作:

def browse
  @title = "Browse"
end
2.1.2 浏览视图

app/views/community/browse.rhtml 中渲染相关部分:

<%= render :partial => "browse_form" %>
<%= render :partial => "result_summary" %>
<%= render :partial => "user_table" %>
2.1.3 浏览表单

使用 Rails 标签助手和 params 变量构建包含年龄、性别和位置字段的表单:

<% form_tag({ :action => "browse" }, :method => "get") do %>
  <fieldset>
    <legend>Browse</legend>
    <div class="form_row">
      <label for="age">Age:</label>
      <%= text_field_tag "min_age", params[:min_age], :size => 2 %>
      &ndash;
      <%= text_field_tag "max_age", params[:max_age], :size => 2 %>
    </div>
    <div class="form_row">
      <label for="gender">Gender:</label>
      <%= radio_button_tag :gender, "Male",
        params[:gender] == 'Male',
        :id => "Male" %>Male
      <%= radio_button_tag :gender, "Female",
        params[:gender] == 'Female',
        :id => "Female" %>Female
    </div>
    <div class="form_row">
      <label for="location">Location:</label>
      Within
      <%= text_field_tag "miles", params[:miles], :size => 4 %>
      miles from zip code:
      <%= text_field_tag "zip_code", params[:zip_code],
        :size => Spec::ZIP_CODE_LENGTH %>
    </div>
    <%= submit_tag "Browse", :class => "submit" %>
  </fieldset>
<% end %>

2.2 按年龄和性别查找用户

app/views/controllers/community_controller.rb 中更新 browse 动作:

def browse
  @title = "Browse"
  return if params[:commit].nil?
  specs = Spec.find_by_asl(params)
  @pages, @users = paginate(specs.collect { |spec| spec.user })
end

return if params[:commit].nil? 用于判断表单是否提交,若未提交则直接渲染浏览表单。

find_by_asl 方法的实现如下:

# Find by age, sex, location.
def self.find_by_asl(params)
  where = []
  # Set up the age restrictions as birthdate range limits in SQL.
  unless params[:min_age].blank?
    where << "ADDDATE(birthdate, INTERVAL :min_age YEAR) < CURDATE()"
  end
  unless params[:max_age].blank?
    where << "ADDDATE(birthdate, INTERVAL :max_age+1 YEAR) > CURDATE()"
  end
  # Set up the gender restriction in SQL.
  where << "gender = :gender" unless params[:gender].blank?

  if where.empty?
    []
  else
    find(:all,
         :conditions => [where.join(" AND "), params],
         :order => "last_name, first_name")
  end
end

该方法使用 MySQL 特定代码,违反了数据库独立性,但能提高查询效率。

3. 地理位置相关功能

3.1 构建本地地理数据库

3.1.1 下载数据

http://www.populardata.com/ 下载免费的邮政编码数据库,Windows 版本可从 http://www.RailsSpace.com/book 获取。下载后解压并重命名为 geo_data.csv ,将其移动到 db/migrate 目录。

3.1.2 创建迁移文件

运行以下命令创建迁移文件:

ruby script/generate migration CreateGeoData

迁移文件 db/migrate/007_create_geo_data.rb 的内容如下:

class CreateGeoData < ActiveRecord::Migration
  def self.up
    create_table :geo_data do |t|
      t.column :zip_code,    :string
      t.column :latitude,    :float
      t.column :longitude,  :float
      t.column :city,        :string
      t.column :state,       :string
      t.column :county,      :string
      t.column :type,        :string
    end
    add_index "geo_data", ["zip_code"], :name => "zip_code_optimization"
    csv_file = "#{RAILS_ROOT}/db/migrate/geo_data.csv"
    fields = '(zip_code, latitude, longitude, city, state, county)'
    execute "LOAD DATA INFILE '#{csv_file}' INTO TABLE geo_data FIELDS " +
            "TERMINATED BY ',' OPTIONALLY ENCLOSED BY \"\"\"\" " +
            "LINES TERMINATED BY '\n' " + fields
  end

  def self.down
    drop_table :geo_data
  end
end

运行迁移命令:

rake db:migrate
3.1.3 创建 GeoDatum 模型

创建 app/models/geo_datum.rb 文件:

class GeoDatum < ActiveRecord::Base
end

3.2 使用地理数据进行位置搜索

3.2.1 计算距离的 SQL 函数

app/models/spec.rb 中添加计算距离的函数:

private
# Return SQL for the distance between a spec's location and the given point.
# See http://en.wikipedia.org/wiki/Haversine_formula for more on the formula.
def self.sql_distance_away(point)
  h = "POWER(SIN((RADIANS(latitude - #{point.latitude}))/2.0),2) + " +
      "COS(RADIANS(#{point.latitude})) * COS(RADIANS(latitude)) * " +
      "POWER(SIN((RADIANS(longitude - #{point.longitude}))/2.0),2)"
  r = 3956 # Earth's radius in miles
  "2 * #{r} * ASIN(SQRT(#{h}))"
end
3.2.2 更新 find_by_asl 方法

app/models/spec.rb 中更新 find_by_asl 方法,添加位置搜索功能:

# Find by age, sex, location.
def self.find_by_asl(params)
  where = []
  # ... 之前的年龄和性别条件 ...
  where << "gender = :gender" unless params[:gender].blank?

  # Set up the distance restriction in SQL.
  zip_code = params[:zip_code]
  unless zip_code.blank? and params[:miles].blank?
    location = GeoDatum.find_by_zip_code(zip_code)
    distance = sql_distance_away(location)
    where << "#{distance} <= :miles"
  end

  if where.empty?
    []
  else
    find(:all,
         :joins => "LEFT JOIN geo_data ON geo_data.zip_code = specs.zip_code",
         :conditions => [where.join(" AND "), params],
         :order => "last_name, first_name")
  end
end

通过左连接 geo_data specs 表,为每个 spec 赋予纬度和经度属性。

3.3 优化位置名称显示

3.3.1 处理城市名称大小写

lib/string.rb 中添加处理城市名称大小写的方法:

class String
  # Capitalize each word (space separated).
  def capitalize_each
    space = " "
    split(space).each{ |word| word.capitalize! }.join(space)
  end

  # Capitalize each word in place.
  def capitalize_each!
    replace capitalize_each
  end
end
3.3.2 格式化位置字符串

app/models/spec.rb 中添加格式化位置字符串的方法:

# Return a sensibly formatted location string.
def location
  if not zip_code.blank? and (city.blank? or state.blank?)
    lookup = GeoDatum.find_by_zip_code(zip_code)
    if lookup
      self.city    = lookup.city.capitalize_each if city.blank?
      self.state = lookup.state if state.blank?
    end
  end
  [city, state, zip_code].join(" ")
end

3.4 添加浏览验证

3.4.1 检测无效整数和浮点数

lib/object.rb 中添加检测无效整数和浮点数的方法:

class Object
  # Return true if the object can be converted to a valid integer.
  def valid_int?
    begin
      Integer(self)
      true
    rescue ArgumentError
      false
    end
  end

  # Return true if the object can be converted to a valid float.
  def valid_float?
    begin
      Float(self)
      true
    rescue ArgumentError
      false
    end
  end
end

app/helpers/application_helper.rb 中添加以下代码加载新函数:

module ApplicationHelper
  require 'string'
  require 'object'
  # ...
end

重启 Web 服务器。

3.4.2 验证输入

app/views/controllers/community_controller.rb 中添加 valid_input? 方法:

private
# Return true if the browse form input is valid, false otherwise.
def valid_input?
  @spec = Spec.new

  # Spec validation (with @spec.valid? below) will catch invalid zip codes.
  zip_code = params[:zip_code]
  @spec.zip_code = zip_code
  # There are a good number of zip codes for which we have no information.
  location = GeoDatum.find_by_zip_code(zip_code)
  if @spec.valid? and not zip_code.blank? and location.nil?
    @spec.errors.add(:zip_code, "does not exist in our database")
  end

  # The age strings should convert to valid integers.
  unless params[:min_age].valid_int? and params[:max_age].valid_int?
    @spec.errors.add("Age range")
  end

  # The zip code is necessary if miles are provided.
  miles = params[:miles]
  if miles and not zip_code
    @spec.errors.add(:zip_code, "can't be blank")
  end

  # The number of miles should convert to a valid float.
  unless miles.nil? or miles.valid_float?
    @spec.errors.add("Location radius")
  end

  # The input is valid iff the errors object is empty.
  @spec.errors.empty?
end

更新 browse 方法:

def browse
  @title = "Browse"
  return if params[:commit].nil?
  if valid_input?
    specs = Spec.find_by_asl(params)
    @pages, @users = paginate(specs.collect { |spec| spec.user })
  end
end
3.4.3 显示错误信息

app/views/community/browse.rhtml 中更新错误信息显示:

<%= error_messages_for('spec').sub('prohibited this spec from being saved',
  'occurred') %>
<%= render :partial => "browse_form" %>
<%= render :partial => "result_summary" %>
<%= render :partial => "user_table" %>

3.5 最终社区主页

app/views/community/index.rhtml 中添加浏览和搜索表单:

<h2><%= @title %></h2>
<fieldset>
  <legend>Alphabetical Index</legend>
  <% @letters.each do |letter| %>
    <% letter_class = (letter == @initial) ? "letter_current" : "letter" %>
    <%= link_to(letter, { :action => "index", :id => letter },
      :class => letter_class) %>
  <% end %>
  <br clear="all" />
</fieldset>
<%= render :partial => "result_summary" %>
<%= render :partial => "user_table" %>
<% if @initial.nil? %>
  <%= render :partial => "browse_form" %>
  <%= render :partial => "search_form" %>
<% end %>

总结

通过以上步骤,实现了搜索和浏览功能,包括搜索功能的测试、浏览页面的构建、地理位置数据的处理、位置搜索的实现、位置名称的优化以及浏览表单的验证。这些功能的实现为用户提供了更强大的查找其他用户的能力。

关键步骤总结

步骤 描述
搜索功能测试 创建额外用户,编写测试用例验证搜索和分页功能
浏览页面构建 构建后端动作、视图和表单,实现按年龄和性别查找用户
地理数据库搭建 下载数据,创建迁移文件和模型,将地理数据导入数据库
位置搜索实现 编写计算距离的函数,更新查找方法,添加位置搜索条件
位置名称优化 处理城市名称大小写,格式化位置字符串
浏览验证添加 检测无效输入,验证输入的有效性,显示友好的错误信息
最终主页整合 在主页添加浏览和搜索表单

流程图

graph TD
    A[搜索功能测试] --> B[浏览页面构建]
    B --> C[地理数据库搭建]
    C --> D[位置搜索实现]
    D --> E[位置名称优化]
    E --> F[浏览验证添加]
    F --> G[最终主页整合]

通过这些步骤和优化,能为用户提供更好的搜索和浏览体验,同时提高系统的稳定性和可靠性。

4. 功能拓展与优化建议

4.1 搜索功能拓展

  • 高级搜索 :可以在搜索功能中添加更多的筛选条件,如用户的兴趣爱好、职业等。在 app/views/community/search.rhtml 中添加相应的表单字段,例如:
<div class="form_row">
  <label for="interest">Interest:</label>
  <%= text_field_tag "interest", params[:interest] %>
</div>

app/views/controllers/community_controller.rb search 方法中处理这些新的参数,更新搜索条件。
- 搜索结果排序 :除了默认的排序方式,可以提供更多的排序选项,如按用户活跃度、注册时间等排序。在搜索页面添加排序选择框,根据用户选择的排序方式更新查询的 :order 参数。

4.2 浏览功能优化

  • 分页优化 :目前的分页功能使用默认值,可提供用户自定义每页显示数量的选项。在浏览页面添加一个输入框和一个提交按钮,让用户输入每页显示的数量,在 app/views/controllers/community_controller.rb 中根据用户输入更新分页参数。
  • 搜索历史记录 :记录用户的浏览搜索历史,方便用户快速重复之前的搜索。可以使用会话(session)来存储搜索历史,在浏览页面显示搜索历史列表,用户点击历史记录即可快速执行相同的搜索。

4.3 地理数据处理优化

  • 数据更新 :地理数据可能会随着时间变化,定期更新地理数据库。可以编写一个定时任务,定期从数据源下载最新的地理数据,并执行迁移脚本来更新数据库。
  • 数据缓存 :对于频繁使用的地理数据,如常用的邮政编码对应的经纬度,可以使用缓存技术(如 Redis)来提高查询速度,减少数据库的压力。

4.4 性能优化

  • 数据库索引 :为经常用于查询的字段添加索引,如 specs 表中的 birthdate gender zip_code 等字段,以及 geo_data 表中的 zip_code 字段。在迁移文件中添加索引创建语句,例如:
add_index "specs", ["birthdate"], :name => "birthdate_index"
  • 缓存机制 :对于一些不经常变化的搜索结果,如热门搜索的结果,可以使用缓存来减少数据库查询次数。可以使用 Rails 的缓存功能,如页面缓存或片段缓存。

5. 安全与稳定性考虑

5.1 输入验证

虽然已经添加了基本的输入验证,但仍需进一步加强。例如,验证用户输入的年龄范围是否合理,避免输入负数或过大的年龄。在 valid_input? 方法中添加额外的验证逻辑:

min_age = params[:min_age].to_i
max_age = params[:max_age].to_i
if min_age > max_age
  @spec.errors.add("Age range", "min age cannot be greater than max age")
end

5.2 防止 SQL 注入

在构建查询条件时,使用参数化查询来防止 SQL 注入攻击。目前的代码已经使用了参数化查询,如 :conditions => [where.join(" AND "), params] ,但仍需确保所有用户输入都经过适当的过滤和验证。

5.3 错误处理

在代码中添加更完善的错误处理机制,捕获并记录可能出现的异常。例如,在 find_by_asl 方法中添加异常处理:

def self.find_by_asl(params)
  begin
    where = []
    # ... 原有的查询条件设置 ...
    if where.empty?
      []
    else
      find(:all,
           :joins => "LEFT JOIN geo_data ON geo_data.zip_code = specs.zip_code",
           :conditions => [where.join(" AND "), params],
           :order => "last_name, first_name")
    end
  rescue Exception => e
    # 记录错误日志
    Rails.logger.error("Error in find_by_asl: #{e.message}")
    []
  end
end

5.4 并发处理

当多个用户同时进行搜索和浏览操作时,可能会出现并发问题。可以使用数据库的事务机制来确保数据的一致性,在需要更新数据的操作中使用事务,例如:

Spec.transaction do
  # 执行数据更新操作
end

6. 用户体验优化

6.1 界面设计

  • 响应式设计 :确保搜索和浏览页面在不同设备上都能有良好的显示效果。使用 CSS 框架(如 Bootstrap)来实现响应式布局。
  • 可视化展示 :对于搜索和浏览结果,可以使用图表或地图等可视化方式展示,让用户更直观地了解数据。例如,使用 Google Maps API 展示用户的地理位置分布。

6.2 反馈机制

  • 加载提示 :在进行搜索和浏览操作时,添加加载提示,让用户知道系统正在处理请求。可以使用 JavaScript 实现一个简单的加载动画,在请求开始时显示,请求完成后隐藏。
  • 操作反馈 :当用户进行搜索或浏览操作后,及时给用户反馈操作结果,如搜索到的用户数量、是否成功提交表单等。在页面上显示相应的提示信息。

6.3 易用性优化

  • 快捷键支持 :为常用的操作提供快捷键,如提交搜索表单、切换分页等,提高用户的操作效率。
  • 帮助文档 :在搜索和浏览页面提供帮助文档,解释各个功能的使用方法和注意事项,帮助用户更好地使用系统。

7. 总结与展望

7.1 总结

通过一系列的开发和优化步骤,我们实现了一个功能较为完善的搜索和浏览系统。从搜索功能的测试和实现,到浏览页面的构建和地理位置相关功能的处理,再到功能的拓展、安全稳定性的保障以及用户体验的优化,每个环节都为系统的实用性和可靠性做出了贡献。

7.2 展望

未来可以进一步拓展系统的功能,如集成社交功能,让用户之间可以进行互动;引入机器学习算法,根据用户的搜索和浏览行为提供个性化的推荐;优化系统的性能,以应对更大规模的用户访问。同时,持续关注用户的反馈,不断改进系统,为用户提供更好的服务。

关键优化点总结

优化方向 具体优化点
功能拓展 高级搜索、搜索结果排序、分页优化、搜索历史记录
地理数据处理 数据更新、数据缓存
性能优化 数据库索引、缓存机制
安全与稳定性 输入验证、防止 SQL 注入、错误处理、并发处理
用户体验 界面设计、反馈机制、易用性优化

流程图

graph TD
    A[功能拓展] --> B[地理数据处理优化]
    B --> C[性能优化]
    C --> D[安全与稳定性保障]
    D --> E[用户体验优化]

通过不断地优化和拓展,我们可以让搜索和浏览系统更加完善,满足用户日益增长的需求。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值