搜索与浏览功能的实现与优化
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–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 %>
–
<%= 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[用户体验优化]
通过不断地优化和拓展,我们可以让搜索和浏览系统更加完善,满足用户日益增长的需求。
超级会员免费看
2013

被折叠的 条评论
为什么被折叠?



