Поиск — это одна из самых распространенных функций практически на любом сайте. Существует множество решений для простого включения поиска в ваше приложение, но в этой статье я расскажу о собственном поиске Postgres в приложениях Rails на основе гема pg_search . Кроме того, я покажу вам, как добавить функцию автозаполнения с помощью плагина select2 .
Я рассмотрю три примера использования функций поиска и автозаполнения в приложениях Rails. В частности, эта статья охватывает:
- построение базовой функции поиска
- обсуждение дополнительных опций, поддерживаемых pg_search
- построение функции автозаполнения для отображения совпадающих имен пользователей
- использование стороннего сервиса для запроса географических местоположений на основе ввода пользователя и включение этой функции с автозаполнением.
Исходный код можно найти на GitHub .
Начиная
Идите вперед и создайте новое приложение Rails. Я буду использовать Rails 5.0.1, но большинство концепций, описанных в этой статье, применимы и к более старым версиям. Пока мы будем использовать поиск Postgres, приложение должно быть инициализировано с помощью адаптера базы данных PostgreSQL:
rails new Autocomplete --database=postgresql
Создайте новую базу данных PG и правильно настройте config/database.yml
. Для исключения моего имени пользователя и пароля в Postgres
из системы контроля версий, я использую камень dotenv-rails :
Gemfile
# ... group :development do gem 'dotenv-rails' end
Чтобы установить его, запустите следующее:
$ bundle install
и создайте файл в корне проекта:
.env
PG_USER: 'user' PG_PASS: 'password'
Затем исключите этот файл из контроля версий:
.gitignore
.env
Ваша конфигурация базы данных может выглядеть так:
конфиг / database.yml
development: adapter: postgresql database: autocomplete host: localhost user: < %= ENV['PG_USER'] %> password: < %= ENV['PG_PASS'] %>
Теперь давайте создадим таблицу и наполним ее примерами данных. Вместо того, чтобы придумывать что-то здесь сложное, я просто сгенерирую таблицу users
со столбцами name
и surname
:
$ rails g model User name:string surname:string $ rails db:migrate
Наши примеры пользователей должны иметь разные имена, чтобы мы могли проверить функцию поиска. Поэтому я буду использовать камень Faker :
Gemfile
# ... group :development do gem 'faker' end
Установите его, запустив это:
$ bundle install
Затем seeds.rb
файл seeds.rb
чтобы создать 50 пользователей со случайными именами и фамилиями:
дБ / seeds.rb
50.times do User.create({name: Faker::Name.first_name, surname: Faker::Name.last_name}) end
Запустите скрипт:
$ rails db:seed
Наконец, введите корневой маршрут, контроллер с соответствующим действием и представлением. На данный момент он будет отображать только
все пользователи:
конфиг / routes.rb
# ... resources :users, only: [:index] root to: 'users#index'
users_controller.rb
class UsersController < ApplicationController def index @users = User.all end end
просмотров / пользователей / index.html.erb
<ul> < %= render @users %> </ul>
просмотров / пользователей / _user.html.erb
<li> < %= user.name %> < %= user.surname %> </li>
Это оно; все приготовления сделаны! Теперь мы можем перейти к следующему разделу и добавить функцию поиска в приложение.
Поиск пользователей
То, что я хотел бы сделать, это отобразить поле поиска в верхней части корневой страницы с надписью «Введите имя или фамилию пользователя»
с кнопкой «Поиск!». Когда форма отправлена, должны отображаться только пользователи, соответствующие введенному термину.
Начните с формы:
просмотров / пользователей / index.html.erb
< %= form_tag users_path, method: :get do %> < %= text_field_tag 'term', params[:term], placeholder: "Enter user's name or surname" %> < %= submit_tag 'Search!' %> < % end %>
Теперь нам нужно взять введенный термин и выполнить фактический поиск. Вот когда в игру вступает гем pg_search :
Gemfile
# ... gem 'pg_search'
Не забудьте установить его:
$ bundle install
Pg_search поддерживает два режима: многопользовательский и pg_search_scope . В этой статье я буду использовать последнюю опцию (multisearchable поддерживает те же опции, но позволяет выполнять глобальный поиск на нескольких моделях). Идея довольно проста: мы создаем область поиска, аналогичную областям ActiveRecord. У него есть имя и множество опций, например, какие поля выполнять поиск. Перед этим модуль PgSearch
должен быть включен в модель:
модели / user.rb
# ... include PgSearch pg_search_scope :search_by_full_name, against: [:name, :surname]
В этом примере столбцы :name
и :surname
будут использоваться для поиска.
Теперь настройте действие контроллера и используйте новую область поиска:
users_controller.rb
# ... if params[:term] @users = User.search_by_full_name(params[:term]) else @users = User.all end
Сам вид не требует никаких дополнительных изменений. Теперь вы можете загрузить сервер, посетить корневую страницу и ввести имя или фамилию какого-либо пользователя, и все должно работать правильно.
Дополнительные опции
Наш поиск работает, но это не очень удобно. Например, если я введу только часть имени или фамилии, результаты не будут возвращены. Это можно легко исправить, предоставив дополнительные параметры.
Прежде всего, вам нужно выбрать, какую технику поиска использовать. По умолчанию используется полнотекстовый поиск Postgres, который я буду использовать в этой статье. Два других — поиск триграмм и метафонов , которые требуют установки дополнительных расширений PG. Чтобы установить технику, :using
опцию: :using
:
pg_search_scope :search_by_full_name, against: [:name, :surname], using: [:tsearch]
Каждая техника также принимает различные варианты. Например, давайте включим поиск частичных слов :
модели / user.rb
class User < ApplicationRecord include PgSearch pg_search_scope :search_by_full_name, against: [:name, :surname], using: { tsearch: { prefix: true } }
Теперь, если вы введете «jay» в качестве поискового запроса, вы увидите всех пользователей, полное имя которых содержит «jay». Обратите внимание, что эта функция поддерживается только начиная с Postgres версии 8.4.
Еще один интересный вариант :negation
, которое позволяет получить критерии исключения , добавив !
символ. Например, Bob !Jones
. Включите эту опцию следующим образом:
модели / user.rb
class User < ApplicationRecord include PgSearch pg_search_scope :search_by_full_name, against: [:name, :surname], using: { tsearch: { prefix: true, negation: true } }
Обратите внимание, однако, что иногда эта функция может дать неожиданные результаты. (Подробнее читайте здесь .)
Также было бы здорово визуально выделить найденное совпадение. Это возможно с :highlight
опция :highlight
:
модели / user.rb
# ... include PgSearch pg_search_scope :search_by_full_name, against: [:name, :surname], using: { tsearch: { prefix: true, negation: true, highlight: { start_sel: '<b>', stop_sel: '</b>', } } }
Здесь мы говорим, что выбор должен быть обернут тегами b
. У функции выделения есть некоторые другие доступные опции, которые можно увидеть здесь .
Чтобы функция подсветки работала, нам нужно внести еще два изменения. Во-первых, with_pg_search_highlight
метод with_pg_search_highlight
так:
users_controller.rb
# ... @users = User.search_by_full_name(params[:term]).with_pg_search_highlight
Во-вторых, настройте частичное:
просмотров / пользователей / _user.html.erb
<li> < % if user.respond_to?(:pg_search_highlight) %> < %= user.pg_search_highlight.html_safe %> < % else %> < %= user.name %> < %= user.surname %> < % end %> </li>
Метод pg_search_highlight
возвращает объединенные значения наших полей поиска ( :name
и :surname
) и выделяет найденное совпадение.
Добавление функции автозаполнения
Еще одна популярная функция на многих сайтах — автозаполнение. Давайте посмотрим, как это можно сделать с помощью pg_search и плагина select2 . Предположим, у нас есть страница «Отправить сообщение», где пользователи могут выбирать, кому писать. На самом деле мы не будем строить логику обмена сообщениями, но если вам интересно, как это можно сделать, прочитайте мою статью « Создание системы обмена сообщениями с помощью Rails и ActionCable» .
Добавьте два камня в Gemfile :
Gemfile
# ... gem 'select2-rails' gem 'underscore-rails'
Select2 — это плагин для превращения общих полей выбора в красивые и мощные элементы управления, тогда как Underscore.js предоставит несколько удобных методов. Зацепите необходимые файлы:
JavaScripts / application.js
//= require underscore //= require select2 //= require messages
Файл messages.coffee
будет создан в ближайшее время. Select2 также требует некоторых стилей для правильной работы:
таблицы стилей / application.scss
@import 'select2';
Теперь давайте быстро добавим новый маршрут, контроллер и представление:
конфиг / routes.rb
# ... resources :messages, only: [:new]
messages_controller.rb
class MessagesController < ApplicationController def new end end
просмотров / сообщений / new.html.erb
<%= form_tag '' do %> < %= label_tag 'to' %> < %= select_tag 'to', nil, style: 'width: 100%' %> < % end %>
Форма не закончена, потому что она никуда не будет отправлена. Обратите внимание, что изначально поле выбора не имеет никаких значений; они будут отображаться динамически в зависимости от ввода пользователя.
Теперь CoffeeScript:
JavaScripts / messages.coffee
jQuery(document).on 'turbolinks:load', -> $('#to').select2 ajax: { url: '/users' data: (params) -> { term: params.term } dataType: 'json' delay: 500 processResults: (data, params) -> { results: _.map(data, (el) -> { id: el.id name: "#{el.surname}, #{el.name}" } ) } cache: true } escapeMarkup: (markup) -> markup minimumInputLength: 2 templateResult: (item) -> item.name templateSelection: (item) -> item.name
Я не буду вдаваться во все детали, объясняющие, что делает этот код, так как эта статья не посвящена Select2 и JavaScript в целом. Тем не менее, давайте быстро рассмотрим основные части:
-
ajax
говорит, что введенные данные должны быть асинхронно отправлены на URL/users
ожидая ответа JSON (dataType
). -
delay: 500
означает, что запрос будет задержан на 0,5 секунды после того, как пользователь закончил печатать. -
processResults
объясняет, как обрабатывать данные, полученные с сервера. Я строю массив объектов
содержит идентификаторы и полные имена пользователей. Обратите внимание, что атрибутid
является обязательным. -
escapeMarkup
переопределяет функциюescapeMarkup
по умолчанию, чтобы предотвратить побег. -
minimumInputLength
предусматривает, что пользователь должен ввести как минимум 2 символа. -
templateResult
определяет внешний вид параметров, отображаемых в раскрывающемся списке. -
templateSelection
определяет внешний вид выбранной опции.
Следующим шагом является настройка действия users#index
наших users#index
позволяющего ему отвечать JSON:
users_controller.rb
# ... def index respond_to do |format| if params[:term] @users = User.search_by_full_name(params[:term]).with_pg_search_highlight else @users = User.all end format.json format.html end end
И наконец, мнение:
просмотров / пользователей / index.json.jbuilder
json.array! @users do |user| json.id user.id json.name user.name json.surname user.surname end
Обратите внимание, что для того, чтобы это работало, необходим гем jBuilder . Для приложений Rails 5 он присутствует в Gemfile по умолчанию.
Пока все хорошо, но разве мы не можем стать немного лучше? Помните ту функцию выделения, которая была добавлена в предыдущем разделе? Давайте использовать это здесь! То, что я хочу сделать, это выделить найденное совпадение в раскрывающемся списке, но не тогда, когда опция уже выбрана. Поэтому нам нужно сохранить поля name
и surname
в ответе JSON, а также ввести поле full_name
:
просмотров / пользователей / index.json.jbuilder
json.array! @users do |user| json.id user.id json.full_name user.pg_search_highlight.html_safe json.name user.name json.surname user.surname end
Теперь processResults
CoffeeScript, изменив processResults
на следующее:
processResults: (data, params) -> { results: _.map(data, (el) -> { name_highlight: el.full_name id: el.id name: "#{el.surname}, #{el.name}" } ) }
Также измените templateResult
на это:
templateResult: (item) -> item.name_highlight
Обратите внимание, что если вы не переопределите функцию escapeMarkup
, все выходные данные будут экранированы, а теги b
используемые для выделения, будут отображаться в виде простого текста.
Вот и все. Не стесняйтесь поиграть с этой функцией и расширить ее!
Автозаполнение сторонним сервисом
Студенты иногда спрашивают меня, как построить поле с функцией автозаполнения, которая позволяет пользователям выбирать свое местоположение. В качестве бонуса я покажу вам одно из возможных решений в этом разделе. Для поиска городов я буду использовать geonames.org , бесплатный сервис, который предоставляет информацию практически обо всем: о больших городах и малых городах, странах, почтовых индексах, различных исторических местах и т. Д. Все, что вам нужно сделать, чтобы начать используя его, зарегистрируйтесь и включите доступ API внутри своего профиля.
Начните с добавления нового маршрута, контроллера и просмотра:
конфиг / routes.rb
# ... resources :cities, only: [:index]
cities_controller.rb
class CitiesController < ApplicationController def index end end
просмотры / город / index.html.erb
<%= form_tag '' do %> < %= label_tag 'city' %> < %= select_tag 'city', '', style: 'width: 100%;' %> < % end %>
Код CoffeeScript будет очень похож на код для добавления функции автозаполнения для страницы сообщений:
cities.coffee
template = (item) -> item.text jQuery(document).on 'turbolinks:load', -> $('#city').select2 ajax: { url: 'http://ws.geonames.org/searchJSON' data: (params) -> { name: params.term, username: 'your_name', featureClass: 'p', cities: 'cities1000' } dataType: 'json' delay: 500 processResults: (data, params) -> { results: _.map(data.geonames, (el) -> name = el.name + ' (' + el.adminName1 + ', ' + el.countryName + ')' { text: name id: name } ) } cache: true } escapeMarkup: (markup) -> markup minimumInputLength: 2 templateResult: template templateSelection: template
Три части, которые имеют изменения: url
, data
и processResults
. Что касается url
, мы используем метод API searchJSON . Этот метод поддерживает различные параметры, чтобы определить, какие объекты и какого типа мы хотим искать. Их параметры передаются внутри атрибута data
:
-
name
— обязательный параметр, который может содержать полное или частичное имя места. -
username
указывает вашу учетную запись GeoNames. -
featureClass
: пока GeoNames имеет огромную базу данных с различными географическими объектами, мы хотим сузить
область поиска. Класс объектовP
означает, что мы хотим искать только города, поселки и деревни. Другие классы объектов можно найти здесь . -
cities
: я ограничил поиск только городами с населением не менее 1000 человек. Это сделано потому, что слишком много маленьких городов и деревень с одинаковыми названиями. Например, мне удалось найти как минимум четыре места под названием «Москва».
В processResults
мы берем возвращенные географические названия и создаем строку, которая будет отображаться в раскрывающемся списке. Для каждого города мы показываем его название, район и страну.
Вот и все, так что теперь вы можете наблюдать эту функцию в действии!
Вывод
В этой статье мы увидели плагины pg_search gem и select2 в действии. С их помощью мы создали функцию поиска и добавили функцию автозаполнения в наше приложение, сделав его немного более удобным для пользователя. Pg_search может предложить гораздо больше интересных вещей, поэтому обязательно просмотрите его документы .
Какие поисковые решения вы используете? Каковы их ключевые особенности? Поделитесь своим опытом в комментариях!