search-engine-for-typesense 30.1.6.18 → 30.1.7.0
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.
- checksums.yaml +4 -4
- data/README.md +30 -0
- data/lib/search_engine/active_record_syncable.rb +14 -3
- data/lib/search_engine/config.rb +11 -2
- data/lib/search_engine/relation/dsl/eval.rb +62 -0
- data/lib/search_engine/relation/dsl/geo.rb +139 -0
- data/lib/search_engine/relation/dsl.rb +4 -0
- data/lib/search_engine/result.rb +25 -1
- data/lib/search_engine/schema.rb +4 -1
- data/lib/search_engine/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9eea6beb086957bbee9df77008a70750be3a74f6f39a53be361a3f1ae8295219
|
|
4
|
+
data.tar.gz: 0eaf79b97f1c68b7bdab350f11cbfe09eecfb4944c76fd5c49528b68e63a5f09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab9dd851d14c4a7019902afad59af281623576952ade57dd2a7df7d458caf68a3f432ac742873b52bc94fd8a88b14ef8715807d7ab3d7d7fd44cc05765424489
|
|
7
|
+
data.tar.gz: 546d394202e194705c24f1c2584b57239bca5e1084d299a3c801fc2781c480d666e47e2906992e6885c788a12ec9162c57e18b1e7eb88e65e93d2a661a2d27f3
|
data/README.md
CHANGED
|
@@ -116,6 +116,36 @@ SearchEngine::Product.upsert_bulk(records: Product.limit(2))
|
|
|
116
116
|
|
|
117
117
|
# Bulk upsert mapped payloads
|
|
118
118
|
SearchEngine::Product.upsert_bulk(data: [mapped])
|
|
119
|
+
|
|
120
|
+
# Geo search
|
|
121
|
+
class SearchEngine::Venue < SearchEngine::Base
|
|
122
|
+
collection :venues
|
|
123
|
+
identify_by :id
|
|
124
|
+
|
|
125
|
+
attribute :name, :string
|
|
126
|
+
attribute :location, :geopoint
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Filter by radius
|
|
130
|
+
SearchEngine::Venue
|
|
131
|
+
.where_geo(:location, within_radius: { lat: 54.69, lng: 25.28, radius: "10 km" })
|
|
132
|
+
.order_geo(:location, from: { lat: 54.69, lng: 25.28 })
|
|
133
|
+
.to_a
|
|
134
|
+
|
|
135
|
+
# Filter by polygon (viewport)
|
|
136
|
+
SearchEngine::Venue
|
|
137
|
+
.where_geo(:location, within_polygon: [[54.72, 25.35], [54.72, 25.22], [54.67, 25.22], [54.67, 25.35]])
|
|
138
|
+
.to_a
|
|
139
|
+
|
|
140
|
+
# Viewport boost with _eval() + distance tiebreaker
|
|
141
|
+
SearchEngine::Venue
|
|
142
|
+
.order_eval("location:(54.72,25.35, 54.72,25.22, 54.67,25.22, 54.67,25.35)", direction: :desc)
|
|
143
|
+
.order_geo(:location, from: { lat: 54.69, lng: 25.28 })
|
|
144
|
+
.to_a
|
|
145
|
+
|
|
146
|
+
# Access geo distance on results (present when order_geo is used)
|
|
147
|
+
result = SearchEngine::Venue.all.order_geo(:location, from: { lat: 54.69, lng: 25.28 }).execute
|
|
148
|
+
result.hits.first.geo_distance_meters # => { "location" => 1234 }
|
|
119
149
|
```
|
|
120
150
|
|
|
121
151
|
## Documentation
|
|
@@ -235,10 +235,21 @@ module SearchEngine
|
|
|
235
235
|
end
|
|
236
236
|
|
|
237
237
|
actions = cfg[:actions]
|
|
238
|
+
timing = begin
|
|
239
|
+
SearchEngine.config.syncable_callback_timing
|
|
240
|
+
rescue StandardError
|
|
241
|
+
:after_commit
|
|
242
|
+
end
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
if timing == :after_commit
|
|
245
|
+
ar_klass.after_create_commit :__se_syncable_upsert! if actions.include?(:create)
|
|
246
|
+
ar_klass.after_update_commit :__se_syncable_upsert! if actions.include?(:update)
|
|
247
|
+
ar_klass.after_destroy_commit :__se_syncable_delete! if actions.include?(:destroy)
|
|
248
|
+
else
|
|
249
|
+
ar_klass.after_create :__se_syncable_upsert! if actions.include?(:create)
|
|
250
|
+
ar_klass.after_update :__se_syncable_upsert! if actions.include?(:update)
|
|
251
|
+
ar_klass.after_destroy :__se_syncable_delete! if actions.include?(:destroy)
|
|
252
|
+
end
|
|
242
253
|
|
|
243
254
|
ar_klass.instance_variable_set(:@__se_syncable_callbacks_installed__, true)
|
|
244
255
|
nil
|
data/lib/search_engine/config.rb
CHANGED
|
@@ -56,6 +56,10 @@ module SearchEngine
|
|
|
56
56
|
# @return [String, nil, false] path to host app SearchEngine models directory. May be
|
|
57
57
|
# relative to `Rails.root` (e.g., "app/search_engine") or absolute. When `nil` or
|
|
58
58
|
# `false`, gem-managed loading of host SearchEngine models is disabled.
|
|
59
|
+
# @!attribute [rw] syncable_callback_timing
|
|
60
|
+
# @return [Symbol] controls ActiveRecordSyncable callback timing.
|
|
61
|
+
# +:after_commit+ (default) uses +after_*_commit+ callbacks (safe, post-transaction).
|
|
62
|
+
# +:after_save+ uses legacy +after_*+ callbacks (in-transaction).
|
|
59
63
|
attr_accessor :logger,
|
|
60
64
|
:default_query_by,
|
|
61
65
|
:default_infix,
|
|
@@ -67,7 +71,8 @@ module SearchEngine
|
|
|
67
71
|
:client,
|
|
68
72
|
:default_console_model,
|
|
69
73
|
:search_engine_models,
|
|
70
|
-
:relation_print_materializes
|
|
74
|
+
:relation_print_materializes,
|
|
75
|
+
:syncable_callback_timing
|
|
71
76
|
|
|
72
77
|
# Lightweight nested configuration for schema lifecycle.
|
|
73
78
|
class SchemaConfig
|
|
@@ -402,6 +407,9 @@ module SearchEngine
|
|
|
402
407
|
@search_engine_models = 'app/search_engine'
|
|
403
408
|
# When true, Relation#inspect/pretty_print materialize a preview (AR-like).
|
|
404
409
|
@relation_print_materializes = true
|
|
410
|
+
# Controls whether ActiveRecordSyncable uses after_*_commit (safe, default)
|
|
411
|
+
# or after_* (legacy in-transaction) callbacks. Values: :after_commit, :after_save.
|
|
412
|
+
@syncable_callback_timing = :after_commit
|
|
405
413
|
end
|
|
406
414
|
|
|
407
415
|
# Whether the engine should avoid network I/O and use an offline client.
|
|
@@ -701,7 +709,8 @@ module SearchEngine
|
|
|
701
709
|
presets: presets_hash_for_to_h,
|
|
702
710
|
curation: curation_hash_for_to_h,
|
|
703
711
|
embedding: embedding_hash_for_to_h,
|
|
704
|
-
relation_print_materializes: relation_print_materializes ? true : false
|
|
712
|
+
relation_print_materializes: relation_print_materializes ? true : false,
|
|
713
|
+
syncable_callback_timing: syncable_callback_timing
|
|
705
714
|
}
|
|
706
715
|
end
|
|
707
716
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Relation
|
|
5
|
+
module DSL
|
|
6
|
+
# Typesense `_eval()` conditional sort expressions.
|
|
7
|
+
# Mixed into Relation's DSL; preserves copy-on-write semantics.
|
|
8
|
+
module Eval
|
|
9
|
+
# Sort by a Typesense `_eval()` conditional expression.
|
|
10
|
+
#
|
|
11
|
+
# Accepts a plain filter-syntax string (simple form) or an Array of
|
|
12
|
+
# `{ expr:, weight: }` hashes (weighted multi-expression form).
|
|
13
|
+
#
|
|
14
|
+
# @param expression [String, Array<Hash>] filter expression(s)
|
|
15
|
+
# @param direction [Symbol] `:desc` (default, matches first) or `:asc`
|
|
16
|
+
# @return [SearchEngine::Relation]
|
|
17
|
+
def order_eval(expression, direction: :desc)
|
|
18
|
+
dir = direction.to_s.downcase
|
|
19
|
+
unless %w[asc desc].include?(dir)
|
|
20
|
+
raise ArgumentError, "order_eval: direction must be :asc or :desc (got #{direction.inspect})"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sort_token = case expression
|
|
24
|
+
when String
|
|
25
|
+
raise ArgumentError, 'order_eval: expression must not be blank' if expression.strip.empty?
|
|
26
|
+
|
|
27
|
+
"_eval(#{expression}):#{dir}"
|
|
28
|
+
when Array
|
|
29
|
+
validate_weighted_expressions!(expression)
|
|
30
|
+
weighted = expression.map { |e| "(#{e[:expr]}):#{e[:weight]}" }.join(', ')
|
|
31
|
+
"_eval([ #{weighted} ]):#{dir}"
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
'order_eval: expression must be a String or Array of { expr:, weight: }'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
spawn do |s|
|
|
38
|
+
existing = Array(s[:orders])
|
|
39
|
+
s[:orders] = dedupe_orders_last_wins(existing + [sort_token])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def validate_weighted_expressions!(expressions)
|
|
46
|
+
unless expressions.is_a?(Array) && !expressions.empty? && expressions.all? { |e| e.is_a?(Hash) }
|
|
47
|
+
raise ArgumentError, 'order_eval: weighted form expects a non-empty Array of { expr:, weight: }'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
expressions.each_with_index do |entry, i|
|
|
51
|
+
unless entry[:expr].is_a?(String) && !entry[:expr].strip.empty?
|
|
52
|
+
raise ArgumentError, "order_eval: entry #{i} must have a non-blank :expr"
|
|
53
|
+
end
|
|
54
|
+
unless entry[:weight].is_a?(Integer) && entry[:weight].positive?
|
|
55
|
+
raise ArgumentError, "order_eval: entry #{i} :weight must be a positive Integer"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Relation
|
|
5
|
+
module DSL
|
|
6
|
+
# Geo search chainers: filtering by radius/polygon and geo distance sorting.
|
|
7
|
+
# Mixed into Relation's DSL; preserves copy-on-write semantics.
|
|
8
|
+
# See DSL::Eval for `_eval()`-based conditional sort expressions.
|
|
9
|
+
module Geo
|
|
10
|
+
# Filter by geographic proximity (radius) or containment (polygon).
|
|
11
|
+
#
|
|
12
|
+
# @param field [Symbol, String] a `:geopoint` or `[:geopoint]` attribute
|
|
13
|
+
# @param within_radius [Hash, nil] `{ lat:, lng:, radius: "10 km" }`
|
|
14
|
+
# @param within_polygon [Array<Array(Numeric,Numeric)>, nil] three or more `[lat, lng]` pairs
|
|
15
|
+
# @return [SearchEngine::Relation]
|
|
16
|
+
def where_geo(field, within_radius: nil, within_polygon: nil)
|
|
17
|
+
validate_geo_field!(field)
|
|
18
|
+
validate_geo_predicate_exclusivity!(within_radius, within_polygon)
|
|
19
|
+
|
|
20
|
+
fragment = if within_radius
|
|
21
|
+
build_radius_filter(field, within_radius)
|
|
22
|
+
else
|
|
23
|
+
build_polygon_filter(field, within_polygon)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
spawn do |s|
|
|
27
|
+
s[:ast] = Array(s[:ast]) + [SearchEngine::AST.raw(fragment)]
|
|
28
|
+
s[:filters] = Array(s[:filters])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Sort by geographic distance from a reference point.
|
|
33
|
+
#
|
|
34
|
+
# @param field [Symbol, String] a `:geopoint` or `[:geopoint]` attribute
|
|
35
|
+
# @param from [Hash] `{ lat:, lng: }` — the reference point
|
|
36
|
+
# @param direction [Symbol] `:asc` (nearest first, default) or `:desc`
|
|
37
|
+
# @param exclude_radius [String, nil] e.g. `"2 km"` — exclude results within this radius from distance scoring
|
|
38
|
+
# @param precision [String, nil] e.g. `"1 km"` — bucket precision for distance sort
|
|
39
|
+
# @return [SearchEngine::Relation]
|
|
40
|
+
def order_geo(field, from:, direction: :asc, exclude_radius: nil, precision: nil)
|
|
41
|
+
validate_geo_field!(field, context: 'order_geo')
|
|
42
|
+
|
|
43
|
+
lat = from[:lat]
|
|
44
|
+
lng = from[:lng]
|
|
45
|
+
validate_geo_coordinate!(lat, lng, context: 'order_geo')
|
|
46
|
+
|
|
47
|
+
dir = direction.to_s.downcase
|
|
48
|
+
unless %w[asc desc].include?(dir)
|
|
49
|
+
raise ArgumentError, "order_geo: direction must be :asc or :desc (got #{direction.inspect})"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sort_token = build_geo_sort_token(field, lat, lng, dir, exclude_radius, precision)
|
|
53
|
+
|
|
54
|
+
spawn do |s|
|
|
55
|
+
existing = Array(s[:orders])
|
|
56
|
+
s[:orders] = dedupe_orders_last_wins(existing + [sort_token])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def validate_geo_field!(field, context: 'where_geo')
|
|
63
|
+
attrs = safe_attributes_map
|
|
64
|
+
return unless attrs && !attrs.empty?
|
|
65
|
+
|
|
66
|
+
type = attrs[field.to_sym]
|
|
67
|
+
return if [:geopoint, [:geopoint]].include?(type)
|
|
68
|
+
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"#{context}: field :#{field} must be declared as :geopoint or [:geopoint]"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_geo_predicate_exclusivity!(within_radius, within_polygon)
|
|
74
|
+
if within_radius.nil? && within_polygon.nil?
|
|
75
|
+
raise ArgumentError, 'where_geo: provide either within_radius: or within_polygon:'
|
|
76
|
+
end
|
|
77
|
+
return unless within_radius && within_polygon
|
|
78
|
+
|
|
79
|
+
raise ArgumentError, 'where_geo: within_radius: and within_polygon: are mutually exclusive'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_geo_coordinate!(lat, lng, context: 'where_geo')
|
|
83
|
+
unless lat.is_a?(Numeric) && lat >= -90 && lat <= 90
|
|
84
|
+
raise ArgumentError, "#{context}: lat must be a number in [-90, 90] (got #{lat.inspect})"
|
|
85
|
+
end
|
|
86
|
+
return if lng.is_a?(Numeric) && lng >= -180 && lng <= 180
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "#{context}: lng must be a number in [-180, 180] (got #{lng.inspect})"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_radius!(radius, context: 'where_geo')
|
|
92
|
+
return if radius.is_a?(String) && radius.match?(/\A\d+(\.\d+)?\s*(km|mi)\z/)
|
|
93
|
+
|
|
94
|
+
raise ArgumentError,
|
|
95
|
+
"#{context}: radius must be a string like '10 km' or '5 mi' (got #{radius.inspect})"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_radius_filter(field, opts)
|
|
99
|
+
lat = opts[:lat]
|
|
100
|
+
lng = opts[:lng]
|
|
101
|
+
radius = opts[:radius]
|
|
102
|
+
validate_geo_coordinate!(lat, lng)
|
|
103
|
+
validate_radius!(radius)
|
|
104
|
+
"#{field}:(#{lat}, #{lng}, #{radius})"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build_polygon_filter(field, points)
|
|
108
|
+
unless points.is_a?(Array) && points.size >= 3
|
|
109
|
+
raise ArgumentError, "where_geo: polygon must have >= 3 points (got #{points&.size || 0})"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
points.each_with_index do |point, i|
|
|
113
|
+
unless point.is_a?(Array) && point.size == 2
|
|
114
|
+
raise ArgumentError, "where_geo: polygon point #{i} must be [lat, lng]"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
validate_geo_coordinate!(point[0], point[1])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
coords = points.map { |p| "#{p[0]}, #{p[1]}" }.join(', ')
|
|
121
|
+
"#{field}:(#{coords})"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_geo_sort_token(field, lat, lng, dir, exclude_radius, precision)
|
|
125
|
+
parts = +"#{field}(#{lat}, #{lng}"
|
|
126
|
+
if exclude_radius
|
|
127
|
+
validate_radius!(exclude_radius, context: 'order_geo')
|
|
128
|
+
parts << ", exclude_radius: #{exclude_radius}"
|
|
129
|
+
end
|
|
130
|
+
if precision
|
|
131
|
+
validate_radius!(precision, context: 'order_geo')
|
|
132
|
+
parts << ", precision: #{precision}"
|
|
133
|
+
end
|
|
134
|
+
"#{parts}):#{dir}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'search_engine/relation/dsl/eval'
|
|
3
4
|
require 'search_engine/relation/dsl/filters'
|
|
5
|
+
require 'search_engine/relation/dsl/geo'
|
|
4
6
|
require 'search_engine/relation/dsl/selection'
|
|
5
7
|
require 'search_engine/relation/dsl/vectors'
|
|
6
8
|
|
|
@@ -9,7 +11,9 @@ module SearchEngine
|
|
|
9
11
|
# User-facing chainers and input normalizers.
|
|
10
12
|
# Chainers MUST be copy-on-write and return new Relation instances.
|
|
11
13
|
module DSL
|
|
14
|
+
include SearchEngine::Relation::DSL::Eval
|
|
12
15
|
include SearchEngine::Relation::DSL::Filters
|
|
16
|
+
include SearchEngine::Relation::DSL::Geo
|
|
13
17
|
include SearchEngine::Relation::DSL::Selection
|
|
14
18
|
include SearchEngine::Relation::DSL::Vectors
|
|
15
19
|
|
data/lib/search_engine/result.rb
CHANGED
|
@@ -94,6 +94,7 @@ module SearchEngine
|
|
|
94
94
|
|
|
95
95
|
obj = hydrate(entry[:document])
|
|
96
96
|
attach_highlighting!(obj, entry)
|
|
97
|
+
attach_geo_distance!(obj, entry)
|
|
97
98
|
hydrated << obj
|
|
98
99
|
end
|
|
99
100
|
@hits = hydrated.freeze
|
|
@@ -284,6 +285,16 @@ module SearchEngine
|
|
|
284
285
|
end
|
|
285
286
|
end
|
|
286
287
|
|
|
288
|
+
# Per-hit geo distance mixin: added onto hydrated objects when Typesense
|
|
289
|
+
# returns geo_distance_meters metadata (present when sort_by includes a
|
|
290
|
+
# geopoint distance sort).
|
|
291
|
+
module GeoDistance
|
|
292
|
+
# @return [Hash{String=>Numeric}, nil] mapping of geo field name to distance in meters
|
|
293
|
+
def geo_distance_meters
|
|
294
|
+
instance_variable_get(:@__se_geo_distance__)&.dup
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
287
298
|
def parse_facets
|
|
288
299
|
@__facets_parsed_memo || {}.freeze
|
|
289
300
|
end
|
|
@@ -389,7 +400,9 @@ module SearchEngine
|
|
|
389
400
|
next unless doc
|
|
390
401
|
|
|
391
402
|
obj = hydrate(doc)
|
|
392
|
-
|
|
403
|
+
sym_sub = symbolize_hit(sub)
|
|
404
|
+
attach_highlighting!(obj, sym_sub)
|
|
405
|
+
attach_geo_distance!(obj, sym_sub)
|
|
393
406
|
hydrated << obj
|
|
394
407
|
end
|
|
395
408
|
|
|
@@ -533,6 +546,17 @@ module SearchEngine
|
|
|
533
546
|
obj
|
|
534
547
|
end
|
|
535
548
|
|
|
549
|
+
def attach_geo_distance!(obj, hit_entry)
|
|
550
|
+
raw_geo = hit_entry[:geo_distance_meters]
|
|
551
|
+
return obj unless raw_geo.is_a?(Hash) && !raw_geo.empty?
|
|
552
|
+
|
|
553
|
+
obj.extend(GeoDistance) unless obj.singleton_class.included_modules.include?(GeoDistance)
|
|
554
|
+
obj.instance_variable_set(:@__se_geo_distance__, raw_geo)
|
|
555
|
+
obj
|
|
556
|
+
rescue StandardError
|
|
557
|
+
obj
|
|
558
|
+
end
|
|
559
|
+
|
|
536
560
|
def safe_highlight_ctx
|
|
537
561
|
ctx = @highlight_ctx || {}
|
|
538
562
|
return {} unless ctx.is_a?(Hash)
|
data/lib/search_engine/schema.rb
CHANGED
|
@@ -31,7 +31,8 @@ module SearchEngine
|
|
|
31
31
|
datetime: 'int64',
|
|
32
32
|
time_string: 'string',
|
|
33
33
|
datetime_string: 'string',
|
|
34
|
-
vector: 'float[]'
|
|
34
|
+
vector: 'float[]',
|
|
35
|
+
geopoint: 'geopoint'
|
|
35
36
|
}.freeze
|
|
36
37
|
|
|
37
38
|
FIELD_COMPARE_KEYS = %i[
|
|
@@ -818,11 +819,13 @@ module SearchEngine
|
|
|
818
819
|
s = type_string.to_s
|
|
819
820
|
return 'string[]' if s.casecmp('string[]').zero?
|
|
820
821
|
return 'float[]' if s.casecmp('float[]').zero?
|
|
822
|
+
return 'geopoint[]' if s.casecmp('geopoint[]').zero?
|
|
821
823
|
return 'int64' if s.casecmp('int64').zero?
|
|
822
824
|
return 'int32' if s.casecmp('int32').zero?
|
|
823
825
|
return 'float' if s.casecmp('float').zero?
|
|
824
826
|
return 'bool' if %w[bool boolean].include?(s.downcase)
|
|
825
827
|
return 'string' if s.casecmp('string').zero?
|
|
828
|
+
return 'geopoint' if s.casecmp('geopoint').zero?
|
|
826
829
|
|
|
827
830
|
# Fallback: return as-is
|
|
828
831
|
s
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: search-engine-for-typesense
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 30.1.
|
|
4
|
+
version: 30.1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nikita Shkoda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -178,7 +178,9 @@ files:
|
|
|
178
178
|
- lib/search_engine/relation/compiler.rb
|
|
179
179
|
- lib/search_engine/relation/deletion.rb
|
|
180
180
|
- lib/search_engine/relation/dsl.rb
|
|
181
|
+
- lib/search_engine/relation/dsl/eval.rb
|
|
181
182
|
- lib/search_engine/relation/dsl/filters.rb
|
|
183
|
+
- lib/search_engine/relation/dsl/geo.rb
|
|
182
184
|
- lib/search_engine/relation/dsl/selection.rb
|
|
183
185
|
- lib/search_engine/relation/dsl/vectors.rb
|
|
184
186
|
- lib/search_engine/relation/dx.rb
|