#278 Search with Sunspot
- Download:
- source codeProject Files in Zip (231 KB)
- mp4Full Size H.264 Video (21.6 MB)
- m4vSmaller H.264 Video (11.8 MB)
- webmFull Size VP8 Video (16.1 MB)
- ogvFull Size Theora Video (28.3 MB)
Sunspot은 루비 어플리케이션에서 풀텍스트 검색을 가능케 해 주는 솔루션입니다. Sunspot은 백그라운드에서 Solr를 이용하고 많은 훌륭한 기능들을 가지고 있습니다. 이 연재에서는 이전 연재에서 사용하였던 간단한 블로그 어플리케이션에 풀텍스트 검색기능을 추가해 볼 것입니다.
이 어플리케이션의 한 페이지에는 많은 글들을 보여주는데 여기서 이러한 글들을 검색할 수 있도록 구현하고자 합니다. 이를 위해서 SQL문을 사용하면 금방 어려움에 봉착하기 때문에 종종 최선의 방법이 될 수 없습니다. Sunspot과 같은 전용 풀텍스트 검색 솔루션이 이러한 검색 기능을 구현하는데 훨씬 더 좋은 방법이 됩니다.
Sunspot 설치
Sunspot은 젬 형태로 제공되기 때문에 일반적인 방법대로 Gemfile
에 추가한 후 bundle
명령을 실행합니다.
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'sunspot_rails'
젬이 설치된 후 아래와 같은 명령을 실행하여 Sunspot의 설정 파일을 생성할 필요가 있습니다.
$ rails g sunspot_rails:install
이 명령을 실행하면 /config/sunspot.yml
이라는 YML파일이 생성됩니다. 이 파일내의 기본 설정을 변경할 필요는 없습니다.
Sunspot은 젬 내부에 Solr를 내포하고 있어서 별도로 설치할 필요가 없습니다. 이 말은 별다른 준비과정 없이 바로 사용할 수 있기 때문에 개발시에 매우 편리하다는 것입니다.
$ rake sunspot:solr:start
OS X Lion에서 작업하고 있을 때 자바 런타임이 설치되어 있지 않다면 위의 명령을 실행할 때 설치하라는 프롬프트가 보일 것입니다. 또는 사용중지 경고문이 보일 수 있는데 그냥 무시해도 될 것입니다. 위의 명령이 실행될 때 또한 고급설정에 필요한 몇가지 추가 설정파일을 생성할 것입니다. 여기서는 이것들에 대해서 다루지 않을 것이지만, 이것에 대한 자세한 내용은 이 문서를 참고하기 바랍니다.
Sunspot 사용하기
이제 Sunspot을 설치했으니까 Article
모델에서 이것을 사용해 보도록 하겠습니다. 풀텍스트 검색기능을 추가하기 위해서 searchable
메소드를 사용합니다.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :content end end
이 메소드는 블록을 받아서 그 안에서 검색할 속성을 정의하게 되며 Sunspot은 이를 근거로 어떤 데이터를 인덱스해야 할 지 알게 됩니다. text
메소드를 이용해서 풀텍스트 검색할 속성들을 정의할 수 있습니다. 여기서 사용하고 있는 Article 모델에 대해서는 name과 content 필드를 사용할 것입니다.
Sunspot은 자동으로 새로 추가되는 레코드를 인덱싱하지만 기존의 것은 제외됩니다. 아래의 명령으로 Sunspot이 기존의 레코드를 리인덱싱하도록 할 수 있습니다.
$ rake sunspot:reindex
이제 모든 글들이 Solr 데이터베이스에 존재하게 되고 검색할 수 있게 되었으므로, index 페이지의 상단에 검색 필드를 추가할 것입니다.
<% title "Articles" %> <%= form_tag articles_path, :method => :get do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> <% end %> <!-- rest of view omitted -->
이 폼은 GET 메소드를 이용하여 index 액션으로 서밋되고 이 때 넘겨진 모든 검색 파라미터들이 쿼리 문자열에 추가될 것입니다. 다음으로, 컨트롤러의 index 액션을 변경해서 search
파라미터로 넘어 온 값을 이용해서 검색된 글들을 불러올 것입니다. Sunspot을 이용하여 검색하기 위해서는 이 모델에 대해서 search
명령을 호출해서 블록으로 넘겨 줍니다. 블록 내에서는 여러가지 메소드를 호출하여 복작합 검색을 처리할 수 있습니다. fulltext
메소드에 폼으로부터 넘어 온 search 파마미터를 넘겨 줄 것입니다. 최종적으로는 모든 검색 결과를 @search
에 할당할 것입니다. 이에 대해서 results
메소드를 호출하면 검색된 모든 글의 목록 얻을 수 있습니다.
def index @search = Article.search do fulltext params[:search] end @articles = @search.results end
글을 다시 로딩하여 특정 키워드에 대해서 검색을 해 봄으로써 지금까지 구현한 내용을 테스트해 볼 수 있습니다. 테스트 결과 검색된 모든 글들을 얻을 수 있을 것입니다.
검색결과는 글의 name
이나 content
중에서 해당 키를 포함하는 모든 글들의 목록을 반환하게 됩니다.
이 이외에도 Article
모델 내의 searchable
블록 내에서 할 수 있는 일들이 더 많이 있습니다. 예를 들면, boost
옵션을 이용해서 결과에 대한 가중치를 줄 수 있는데, 글의 name에서 일치하는 것이 content에서 일치하는 것보다 더 중요하게 생각하도록 하는 것이다.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content end end
이것은 관련정도에 따라 결과를 소트하고자 할 때 중요한 것이다. 이 경우에는, 글의 name에 검색 키가 포함되는 글이, content에 검색 키가 포함되는 경우보다 더 상위에 나타날 것입니다.
searchable
블록에 지정하는 속성들은 반드시 실제 데이터베이스의 컬럼명일 필요는 없습니다. 해당 모델에서 정의할 수 있는 어떤 메소드라도 사용할 수 있습니다. 이제 publish_month
라는 메소드를 생성해서 글을 게재할 때의 월 이름과 년도를 포함하는 문자열을 반환하도록 정의할 것입니다. 그러면 이 메소드에 대해서 마치 데이터베이스의 컬럼과 같이 검색할 수 있게 됩니다.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month end def publish_month published_at.strftime("%B %Y") end end
이제 이와 같이 새로 만들어진 컬럼에 대해서, 검색하기 전에 rake sunspot:reindex
명령을 실행하여, 기존의 모든 레코드에 대해서 리인텍싱을 할 필요가 있을 것입니다. 이후에 월 이름으로 글을 검색할 수 있게 됩니다.
메소드를 생성하는 것에 대한 대안으로, 블록 형태를 넘겨 주고 이 블록이 반환하는 값에 대해서 검색할 수 있도록 하는 것입니다. 하나의 글은 여러개의 댓글을 가지게 되는데, 블록을 이용해서 댓글의 content에 대해서도 검색할 수 있도록 할 것입니다.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end end def publish_month published_at.strftime("%B %Y") end end
이 블록내의 컨텍스트는 Article
모델의 한 인스턴스이기 때문에 하나의 글에 속한 댓글들을 불러 와서 각 댓글의 content로 매핑할 수 있습니다. 블록에서 배열을 반환하더라도 Sunspot은 모든 댓글을 인덱싱해서 검색가능하게 해 줍니다.
속성에 대한 검색
단순한 풀텍스트 검색을 넘어서는, 예를 들어 특정 속성에 대한 검색과 같은, 기능을 추가하고자 한다면 어떻게 될까요? 이를 위해서 검색하고자 하는 속성을 데이터형으로 넘겨 줄 수 있습니다. 즉, 문자열, 정수, 부동소수, 심지어 타임스탬프 등과 같은 형태로 넘겨 줄 수 있습니다. 검색 필드에 published_at
속성을 추가하기 위해서는 time
메소드를 사용할 수 있습니다.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end time :published_at end def publish_month published_at.strftime("%B %Y") end end
ArticleController
클래스에서도 이러한 기능을 이용할 수 있어서 현재 시간보다 이전의 published_at
날짜를 가진 글들만 대상으로 검색하도록 할 수 있습니다. with
메소드를 이용하여 이러한 내용을 구현할 수 있습니다.
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) end @articles = @search.results end
With this in place the search won’t return articles that haven’t yet been published. There is some great documentation on the attributes you can pass in on the Sunspot wiki page.
제대로 동작한다면 아직 게재(출판)되지 않은 글들은 검색 대상에 포함되지 않을 것입니다. Sunspot 위키 페이지를 방문하면 넘겨 줄 수 있는 속성들에 대한 자세한 문서를 찾아 볼 수 있습니다.
Faceted 검색하기
Faceted 검색하기를 이용하면 글을 게재한 월과 같은 특정 속성을 기준으로 검색 결과를 필터할 수 있습니다. 글이 게재된 월을 보여주는 링크 목록을 추가한다고 가정해 보겠습니다. 특정 월의 링크를 클릭할 때 해당 월에 게제된 글들만 보이게 될 것입니다.
이를 위해서, searchable
블록에 publish_month
메소드에 대한 string
속성을 우선 추가할 것입니다.
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end time :published_at string :publish_month end def publish_month published_at.strftime("%B %Y") end end
이것을 ArticlesController
클래스의 search
블록 내에서 facet
을 호출하므로써 하나의 facet으로 전환할 수 있습니다.
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) facet(:publish_month) end @articles = @search.results end
이제 검색창과 글 목록 사이에 아래의 코드를 추가하여 index
페이지에 이 facet들을 목록으로 보여 줄 수 있습니다.
<div id="facets"> <h3>Published</h3> <ul> <% for row in @search.facet(:publish_month).rows %> <li> <% if params[:month].blank? %> <%= link_to row.value, :month => row.value %> (<%= row.count %>) <% else %> <strong><%= row.value %></strong> (<%= link_to "remove", :month => nil %>) <% end %> </li> <% end %> </ul> </div>
위의 코드에서, publish_month
facet 항목들이 각각에 대해서 루프를 돌면서 표시됩니다. @search
객체에 대해서 .facet
을 호출하여, facet의 목록을 구분하여 보여주기를 원하는 속성을 넘겨 주고, 이 경우에는 :publish_month
가 해당되는데, 다시 .rows
를 호출하면 해당 속성에 대한 모든 facet 옵션을 반환할 것입니다.
row.value
를 호출하면 해당 속성의 값을 반환하는데, 예를 들면, “January 2011”와 같습니다. 또한 row.count
를 호출하여 해당 조건에 맞는 글들의 갯수를 얻을 수 있습니다. 쿼리 문자열에 month
파라미터가 있을 경우, 해당 파라미터를 제거하기 위한 “remove” 링크와 함께 해당 값을 보여 줄 것입니다. 이렇게 하면 해당 facet을 선택하여 month
파라미터를 넘겨 줄 수 있게 됩니다.
이제 페이지를 다시 로드할 때 레코드를 리인덱스한 경우 패널 형태로 facet 목록을 보게 될 것입니다. 이 때 각 facet은 월 이름과 해당 월에 게제된 글의 수를 보여줄 것입니다. 특정 월을 선택한다면, 쿼리 문자열에서 month
파라미터를 볼 수 있을 것이지만 글은 필터되지 않습니다. 이러한 문제를 해결하기 위해서, 컨트롤러에서 search
에 또 다른 with
파라미터를 추가할 필요가 있고 이렇게 하므로써 month
파라미터가 존재할 경우 월별로 글을 필터할 수 있게 됩니다.
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) facet(:publish_month) with(:publish_month, params[:month]) ↵ if params[:month].present? end @articles = @search.results end
이제 특정 월을 선택할 때, 해당 월에 게재된 글들만 정확하게 보여지게 될 것입니다.
“remove” 링크를 클릭하면 전체 목록을 볼 수 있게 될 것입니다. 이것은 물론 검색결과와 연동해서 동작합니다. 검색 키를 입력하면 해당 키로 검색되는 글들이 게재된 월 목록이 보여지게 될 것입니다.
facet은 검색기능과 함께 중요한 기능입니다.
Sunspot에 대한 내용은 여기까지 입니다. Sunspot은 레일스 어플리케이션에 풀텍스트 검색 기능을 추가하기 위한 좋은 방법이고 여기서 다루지 못한 많은 추가 기능들을 가지고 있습니다. 더 많은 정보를 얻고자 한다면 위키를 찾아보기 바랍니다.