dm-lucene-adapter 0.1.0-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/lib/dm-lucene-adapter.rb +8 -0
- data/lib/dm-lucene-adapter_ext.jar +0 -0
- data/lib/dm_lucene_adapter/dm_lucene_adapter.rb +196 -0
- data/lib/dm_lucene_adapter/version.rb +3 -0
- data/spec/dm_lucene_adapter_spec.rb +148 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +24 -0
- metadata +81 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Kristian Meier
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
Binary file
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
class LuceneAdapter < AbstractAdapter
|
4
|
+
|
5
|
+
private
|
6
|
+
def index_directory(model)
|
7
|
+
@path / "#{model.storage_name(name)}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def lucene(model)
|
11
|
+
LuceneService.new(java.io.File.new(index_directory(model).to_s))
|
12
|
+
end
|
13
|
+
|
14
|
+
public
|
15
|
+
|
16
|
+
def initialize(name, options = {})
|
17
|
+
super
|
18
|
+
(@path = Pathname(@options[:path]).freeze).mkpath
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [Enumerable<Resource>] resources
|
22
|
+
# The list of resources (model instances) to create
|
23
|
+
#
|
24
|
+
# @return [Integer]
|
25
|
+
# The number of records that were actually saved into the data-store
|
26
|
+
#
|
27
|
+
# @api semipublic
|
28
|
+
def create(resources)
|
29
|
+
count = 0
|
30
|
+
indexer = lucene(resources.first.model).create_indexer
|
31
|
+
resources.each do |resource|
|
32
|
+
resource.id = indexer.next_id
|
33
|
+
map = {}
|
34
|
+
resource.attributes.each { |k,v| map[k.to_s] = v.to_s}
|
35
|
+
indexer.index(map)
|
36
|
+
count += 1
|
37
|
+
end
|
38
|
+
count
|
39
|
+
ensure
|
40
|
+
indexer.close if indexer
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Query] query
|
44
|
+
# the query to match resources in the datastore
|
45
|
+
#
|
46
|
+
# @return [Enumerable<Hash>]
|
47
|
+
# an array of hashes to become resources
|
48
|
+
#
|
49
|
+
# @api semipublic
|
50
|
+
def read(query)
|
51
|
+
is_sorting = query.order.size > 1 || query.order[0].target.name != :id || query.order[0].operator != :asc
|
52
|
+
offset, limit = is_sorting ? [0, -1] : [query.offset, query.limit || -1]
|
53
|
+
|
54
|
+
resources = do_read(offset, limit, query)
|
55
|
+
resources.each do |resource|
|
56
|
+
query.fields.each do |field|
|
57
|
+
attribute = field.name.to_s
|
58
|
+
resource[attribute] = field.typecast(resource[attribute])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if is_sorting
|
63
|
+
#records = records.uniq if unique?
|
64
|
+
resources = query.sort_records(resources)
|
65
|
+
resources = query.limit_records(resources)
|
66
|
+
end
|
67
|
+
resources
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def do_read(offset, limit, query)
|
73
|
+
reader = lucene(query.model).create_reader
|
74
|
+
if(query.conditions.nil?)
|
75
|
+
result = []
|
76
|
+
|
77
|
+
#p query
|
78
|
+
# TODO set limit, offset to default of sorting is non default
|
79
|
+
reader.read_all(offset, limit).each do |resource|
|
80
|
+
map = {}
|
81
|
+
resource.each do |k,v|
|
82
|
+
map[k] = v
|
83
|
+
end
|
84
|
+
result << map
|
85
|
+
end
|
86
|
+
result
|
87
|
+
else
|
88
|
+
ops = query.conditions.operands
|
89
|
+
if(ops.size == 1 && ops[0].class == DataMapper::Query::Conditions::EqualToComparison && ops[0].subject.name == :id)
|
90
|
+
map = {}
|
91
|
+
reader.read(query.conditions.operands[0].value).each do |k,v|
|
92
|
+
map[k] = v
|
93
|
+
end
|
94
|
+
[map]
|
95
|
+
else
|
96
|
+
op = query.conditions.slug.to_s.upcase
|
97
|
+
lquery = make_query("", ops, op)
|
98
|
+
lquery.sub!(/#{op} $/, '')
|
99
|
+
result = []
|
100
|
+
reader.read_all(offset, limit, lquery).each do |resource|
|
101
|
+
map = {}
|
102
|
+
resource.each do |k,v|
|
103
|
+
map[k] = v
|
104
|
+
end
|
105
|
+
result << map
|
106
|
+
end
|
107
|
+
result
|
108
|
+
end
|
109
|
+
end
|
110
|
+
ensure
|
111
|
+
reader.close if reader
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def make_query(lquery, ops, operator)
|
117
|
+
ops.each do |comp|
|
118
|
+
case comp.slug
|
119
|
+
when :like
|
120
|
+
lquery += "#{comp.subject.name.to_s}:#{comp.value}"
|
121
|
+
unless comp.value =~ /%|_|\?|\*/
|
122
|
+
lquery += "~"
|
123
|
+
end
|
124
|
+
lquery += " #{operator} "
|
125
|
+
when :eql
|
126
|
+
lquery += "#{comp.subject.name.to_s}:\"#{comp.value}\" #{operator} "
|
127
|
+
when :not
|
128
|
+
if lquery.size == 0
|
129
|
+
lquery = "NOT "
|
130
|
+
else
|
131
|
+
lquery.sub!(/#{operator}/, "NOT")
|
132
|
+
end
|
133
|
+
lquery = make_query(lquery, comp.operands, operator)
|
134
|
+
else
|
135
|
+
warn "ignore unsupported operand #{comp.slug}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
lquery
|
139
|
+
end
|
140
|
+
|
141
|
+
public
|
142
|
+
|
143
|
+
# @param [Hash(Property => Object)] attributes
|
144
|
+
# hash of attribute values to set, keyed by Property
|
145
|
+
# @param [Collection] collection
|
146
|
+
# collection of records to be updated
|
147
|
+
#
|
148
|
+
# @return [Integer]
|
149
|
+
# the number of records updated
|
150
|
+
#
|
151
|
+
# @api semipublic
|
152
|
+
def update(attributes, collection)
|
153
|
+
count = 0
|
154
|
+
service = lucene(collection.model)
|
155
|
+
deleter = service.create_deleter
|
156
|
+
resources = read(collection.query)
|
157
|
+
resources.each do |resource|
|
158
|
+
deleter.delete(resource["id"])
|
159
|
+
end
|
160
|
+
deleter.close
|
161
|
+
deleter = nil
|
162
|
+
indexer = service.create_indexer
|
163
|
+
attributes = attributes_as_fields(attributes)
|
164
|
+
resources.each do |resource|
|
165
|
+
resource.update(attributes)
|
166
|
+
map = {}
|
167
|
+
resource.each { |k,v| map[k.to_s] = v.to_s}
|
168
|
+
indexer.index(map)
|
169
|
+
end
|
170
|
+
count
|
171
|
+
ensure
|
172
|
+
indexer.close if indexer
|
173
|
+
deleter.close if deleter
|
174
|
+
end
|
175
|
+
|
176
|
+
# @param [Collection] collection
|
177
|
+
# collection of records to be deleted
|
178
|
+
#
|
179
|
+
# @return [Integer]
|
180
|
+
# the number of records deleted
|
181
|
+
#
|
182
|
+
# @api semipublic
|
183
|
+
def delete(collection)
|
184
|
+
count = 0
|
185
|
+
indexer = lucene(collection.model).create_deleter
|
186
|
+
collection.each do |resource|
|
187
|
+
indexer.delete(resource.id)
|
188
|
+
count += 1
|
189
|
+
end
|
190
|
+
count
|
191
|
+
ensure
|
192
|
+
indexer.close if indexer
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe DataMapper::Adapters::LuceneAdapter do
|
4
|
+
|
5
|
+
it "should create" do
|
6
|
+
Book.create(:author => "kristian", :title => "me and the corner").new?.should be_false
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should read all" do
|
10
|
+
size = Book.all.size
|
11
|
+
(1..3).each do
|
12
|
+
Book.create(:author => "kristian", :title => "me and the corner")
|
13
|
+
end
|
14
|
+
b = Book.all
|
15
|
+
b.size.should == size + 3
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should delete" do
|
19
|
+
books = []
|
20
|
+
(1..4).each do
|
21
|
+
books << Book.create(:author => "kristian", :title => "me and the corner")
|
22
|
+
end
|
23
|
+
id = books.last.id
|
24
|
+
size = Book.all.size
|
25
|
+
books.each do |b|
|
26
|
+
b.destroy if b.id % 2 == 0
|
27
|
+
end
|
28
|
+
b = Book.all
|
29
|
+
size.should == b.size + 2
|
30
|
+
book = Book.create(:author => "kristian", :title => "me and the corner")
|
31
|
+
id.should < book.id
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should read a single' do
|
35
|
+
book = Book.create(:author => "kristian", :title => "me and the corner")
|
36
|
+
b = Book.get(book.id)
|
37
|
+
b.id.should == book.id
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should update' do
|
41
|
+
book = Book.create(:author => "kristian", :title => "me and the corner")
|
42
|
+
book.author = "sanuka"
|
43
|
+
book.save
|
44
|
+
b = Book.get(book.id)
|
45
|
+
b.author.should == "sanuka"
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "search" do
|
49
|
+
|
50
|
+
before :all do
|
51
|
+
@size = Book.all(:author => "sanuka").size
|
52
|
+
@len = 5
|
53
|
+
(1..@len).each do |i|
|
54
|
+
Book.create(:author => "kristian", :title => "me and the corner #{i}")
|
55
|
+
Book.create(:author => "sanuka", :title => "me and the corner #{i}")
|
56
|
+
Book.create(:author => "#{i}sanuka", :title => "me and the corner")
|
57
|
+
Book.create(:author => "#{i}sanuka#{i}", :title => "me and the corner")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "ascending" do
|
62
|
+
|
63
|
+
after :each do
|
64
|
+
id = 0
|
65
|
+
@books.each do |b|
|
66
|
+
b.id.should > id
|
67
|
+
id = b.id
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should find 5 with simple query' do
|
72
|
+
@books = Book.all(:author => "sanuka")
|
73
|
+
@books.size.should == @size + @len
|
74
|
+
@books.each { |b| b.author.should == "sanuka" }
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should find 5 with not query' do
|
78
|
+
@books = Book.all(:author.not => "sanuka")
|
79
|
+
@books.each { |b| b.author.should_not == "sanuka" }
|
80
|
+
(@books.size + @size + @len).should == Book.all.size
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should find nothing with simple query' do
|
84
|
+
@books = Book.all(:author => "saumya")
|
85
|
+
@books.size.should == 0
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should find 5 with fuzzy query' do
|
89
|
+
@books = Book.all(:author.like => "sanaku")
|
90
|
+
@books.size.should == @size + @len
|
91
|
+
@books.each { |b| b.author.should =~ /sanuka/ }
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should find 5 with single char wildcards query' do
|
95
|
+
@books = Book.all(:author.like => "san?k?")
|
96
|
+
@books.size.should == @size + @len
|
97
|
+
@books.each { |b| b.author.should =~ /sanuka/ }
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should find 5 with wildcards query' do
|
101
|
+
@books = Book.all(:author.like => "sa*ka")
|
102
|
+
@books.size.should == @size + @len
|
103
|
+
@books.each { |b| b.author.should =~ /sanuka/ }
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should obey offset' do
|
107
|
+
@books = Book.all(:author.like => "san?k?", :limit => @size)
|
108
|
+
@books.size.should == @size
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should obey offset with empty query' do
|
112
|
+
@books = Book.all(:limit => @size)
|
113
|
+
@books.size.should == @size
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should obey offset and limit' do
|
117
|
+
@books = Book.all(:author.like => "san?k?", :offset => @size, :limit => @len)
|
118
|
+
@books.size.should == @len
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should obey offset and limit with empty query' do
|
122
|
+
@books = Book.all(:offset => @size, :limit => @len)
|
123
|
+
@books.size.should == @len
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "descending" do
|
128
|
+
|
129
|
+
after :each do
|
130
|
+
id = 12311230123321
|
131
|
+
@books.each do |b|
|
132
|
+
b.id.should < id
|
133
|
+
id = b.id
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should obey offset and limit descending order' do
|
138
|
+
@books = Book.all(:author.like => "san?k?", :offset => @size, :limit => @len, :order => [:id.desc])
|
139
|
+
@books.size.should == @len
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'should obey offset and limit descending order with emtpy query' do
|
143
|
+
@books = Book.all(:offset => @len, :limit => @size, :order => [:id.desc])
|
144
|
+
@books.size.should == @size
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'dm-core', ">0.10.0"
|
4
|
+
require 'pathname'
|
5
|
+
$LOAD_PATH << Pathname(__FILE__).dirname.parent.expand_path + 'lib'
|
6
|
+
|
7
|
+
require 'dm-core'
|
8
|
+
require 'dm-lucene-adapter'
|
9
|
+
|
10
|
+
class Book
|
11
|
+
|
12
|
+
include DataMapper::Resource
|
13
|
+
|
14
|
+
property :id, Serial
|
15
|
+
|
16
|
+
property :author, String, :length => 128
|
17
|
+
|
18
|
+
property :title, String, :length => 255
|
19
|
+
|
20
|
+
property :published, Boolean, :default => true
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
DataMapper.setup(:default, :adapter => :lucene, :path => "tmp")
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-lucene-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: java
|
6
|
+
authors: []
|
7
|
+
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-28 00:00:00 +05:30
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: dm-core
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - "="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.10.1
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "1.2"
|
34
|
+
version:
|
35
|
+
description: datamapper adapter for search index lucene
|
36
|
+
email:
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- lib/dm-lucene-adapter.rb
|
45
|
+
- lib/dm-lucene-adapter_ext.jar
|
46
|
+
- lib/dm_lucene_adapter/dm_lucene_adapter.rb
|
47
|
+
- lib/dm_lucene_adapter/version.rb
|
48
|
+
- spec/dm_lucene_adapter_spec.rb
|
49
|
+
- spec/spec_helper.rb
|
50
|
+
- spec/spec.opts
|
51
|
+
- MIT-LICENSE
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://github.com/mkristian/dm-lucene-adapter
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
requirements: []
|
74
|
+
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.3.5
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
summary: dm adapter for lucene
|
80
|
+
test_files: []
|
81
|
+
|