Статьи

Поиск и автозаполнение в приложениях Rails

Добавление поиска в приложения Rails

Поиск — это одна из самых распространенных функций практически на любом сайте. Существует множество решений для простого включения поиска в ваше приложение, но в этой статье я расскажу о собственном поиске Postgres в приложениях Rails на основе гема pg_search . Кроме того, я покажу вам, как добавить функцию автозаполнения с помощью плагина select2 .

Я рассмотрю три примера использования функций поиска и автозаполнения в приложениях Rails. В частности, эта статья охватывает:

  • построение базовой функции поиска
  • обсуждение дополнительных опций, поддерживаемых pg_search
  • построение функции автозаполнения для отображения совпадающих имен пользователей
  • использование стороннего сервиса для запроса географических местоположений на основе ввода пользователя и включение этой функции с автозаполнением.
Посмотрите 5 методов Ruby Metaprogramming для упрощения вашего кода
Давай запрограммируем

Исходный код можно найти на 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 может предложить гораздо больше интересных вещей, поэтому обязательно просмотрите его документы .

Какие поисковые решения вы используете? Каковы их ключевые особенности? Поделитесь своим опытом в комментариях!