From 982828099e3eb2e8ee71145755607ff0dfb7f133 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 19 May 2025 00:20:02 +0200 Subject: [PATCH] Adding upstream version 2.5.1. Signed-off-by: Daniel Baumann --- .github/workflows/tests.yml | 24 + .gitignore | 20 + .travis.yml | 25 + CONTRIBUTING.md | 16 + LICENSE | 202 + README.md | 121 + SECURITY.md | 15 + analysis/analyzer/custom/custom.go | 148 + analysis/analyzer/keyword/keyword.go | 41 + analysis/analyzer/simple/simple.go | 49 + analysis/analyzer/standard/standard.go | 55 + analysis/analyzer/web/web.go | 55 + analysis/benchmark_test.go | 117 + analysis/char/asciifolding/asciifolding.go | 3572 +++++++++++++ .../char/asciifolding/asciifolding_test.go | 124 + analysis/char/html/html.go | 57 + analysis/char/regexp/regexp.go | 65 + analysis/char/regexp/regexp_test.go | 88 + .../zerowidthnonjoiner/zerowidthnonjoiner.go | 39 + analysis/datetime/flexible/flexible.go | 67 + analysis/datetime/flexible/flexible_test.go | 100 + analysis/datetime/iso/iso.go | 250 + analysis/datetime/iso/iso_test.go | 182 + analysis/datetime/optional/optional.go | 50 + analysis/datetime/percent/percent.go | 205 + analysis/datetime/percent/percent_test.go | 474 ++ analysis/datetime/sanitized/sanitized.go | 130 + analysis/datetime/sanitized/sanitized_test.go | 109 + .../timestamp/microseconds/microseconds.go | 55 + .../timestamp/milliseconds/milliseconds.go | 55 + .../timestamp/nanoseconds/nanoseconds.go | 55 + .../datetime/timestamp/seconds/seconds.go | 55 + analysis/freq.go | 70 + analysis/freq_test.go | 60 + analysis/lang/ar/analyzer_ar.go | 68 + analysis/lang/ar/analyzer_ar_test.go | 184 + analysis/lang/ar/arabic_normalize.go | 88 + analysis/lang/ar/arabic_normalize_test.go | 234 + analysis/lang/ar/stemmer_ar.go | 121 + analysis/lang/ar/stemmer_ar_test.go | 397 ++ analysis/lang/ar/stop_filter_ar.go | 36 + analysis/lang/ar/stop_words_ar.go | 152 + analysis/lang/bg/stop_filter_bg.go | 36 + analysis/lang/bg/stop_words_bg.go | 220 + analysis/lang/ca/articles_ca.go | 33 + analysis/lang/ca/elision_ca.go | 40 + analysis/lang/ca/elision_ca_test.go | 61 + analysis/lang/ca/stop_filter_ca.go | 36 + analysis/lang/ca/stop_words_ca.go | 247 + analysis/lang/cjk/analyzer_cjk.go | 60 + analysis/lang/cjk/analyzer_cjk_test.go | 642 +++ analysis/lang/cjk/cjk_bigram.go | 210 + analysis/lang/cjk/cjk_bigram_test.go | 848 +++ analysis/lang/cjk/cjk_width.go | 104 + analysis/lang/cjk/cjk_width_test.go | 93 + analysis/lang/ckb/analyzer_ckb.go | 64 + analysis/lang/ckb/analyzer_ckb_test.go | 77 + analysis/lang/ckb/sorani_normalize.go | 121 + analysis/lang/ckb/sorani_normalize_test.go | 323 ++ analysis/lang/ckb/sorani_stemmer_filter.go | 151 + .../lang/ckb/sorani_stemmer_filter_test.go | 299 ++ analysis/lang/ckb/stop_filter_ckb.go | 36 + analysis/lang/ckb/stop_words_ckb.go | 163 + analysis/lang/cs/stop_filter_cs.go | 36 + analysis/lang/cs/stop_words_cs.go | 199 + analysis/lang/da/analyzer_da.go | 59 + analysis/lang/da/analyzer_da_test.go | 71 + analysis/lang/da/stemmer_da.go | 52 + analysis/lang/da/stop_filter_da.go | 36 + analysis/lang/da/stop_words_da.go | 137 + analysis/lang/de/analyzer_de.go | 64 + analysis/lang/de/analyzer_de_test.go | 155 + analysis/lang/de/german_normalize.go | 98 + analysis/lang/de/german_normalize_test.go | 103 + analysis/lang/de/light_stemmer_de.go | 119 + analysis/lang/de/stemmer_de_snowball.go | 52 + analysis/lang/de/stemmer_de_test.go | 91 + analysis/lang/de/stop_filter_de.go | 36 + analysis/lang/de/stop_words_de.go | 321 ++ analysis/lang/el/stop_filter_el.go | 36 + analysis/lang/el/stop_words_el.go | 105 + analysis/lang/en/analyzer_en.go | 73 + analysis/lang/en/analyzer_en_test.go | 105 + analysis/lang/en/plural_stemmer.go | 177 + analysis/lang/en/plural_stemmer_test.go | 46 + analysis/lang/en/possessive_filter_en.go | 70 + analysis/lang/en/possessive_filter_en_test.go | 142 + analysis/lang/en/stemmer_en_snowball.go | 52 + analysis/lang/en/stemmer_en_test.go | 79 + analysis/lang/en/stop_filter_en.go | 36 + analysis/lang/en/stop_words_en.go | 347 ++ analysis/lang/es/analyzer_es.go | 66 + analysis/lang/es/analyzer_es_test.go | 122 + analysis/lang/es/light_stemmer_es.go | 78 + analysis/lang/es/spanish_normalize.go | 70 + analysis/lang/es/spanish_normalize_test.go | 112 + analysis/lang/es/stemmer_es_snowball.go | 52 + analysis/lang/es/stemmer_es_snowball_test.go | 79 + analysis/lang/es/stop_filter_es.go | 36 + analysis/lang/es/stop_words_es.go | 383 ++ analysis/lang/eu/stop_filter_eu.go | 36 + analysis/lang/eu/stop_words_eu.go | 126 + analysis/lang/fa/analyzer_fa.go | 74 + analysis/lang/fa/analyzer_fa_test.go | 684 +++ analysis/lang/fa/persian_normalize.go | 80 + analysis/lang/fa/persian_normalize_test.go | 130 + analysis/lang/fa/stop_filter_fa.go | 36 + analysis/lang/fa/stop_words_fa.go | 340 ++ analysis/lang/fi/analyzer_fi.go | 60 + analysis/lang/fi/analyzer_fi_test.go | 70 + analysis/lang/fi/stemmer_fi.go | 52 + analysis/lang/fi/stop_filter_fi.go | 36 + analysis/lang/fi/stop_words_fi.go | 124 + analysis/lang/fr/analyzer_fr.go | 65 + analysis/lang/fr/analyzer_fr_test.go | 209 + analysis/lang/fr/articles_fr.go | 40 + analysis/lang/fr/elision_fr.go | 40 + analysis/lang/fr/elision_fr_test.go | 55 + analysis/lang/fr/light_stemmer_fr.go | 309 ++ analysis/lang/fr/light_stemmer_fr_test.go | 1015 ++++ analysis/lang/fr/minimal_stemmer_fr.go | 82 + analysis/lang/fr/minimal_stemmer_fr_test.go | 139 + analysis/lang/fr/stemmer_fr_snowball.go | 52 + analysis/lang/fr/stemmer_fr_snowball_test.go | 79 + analysis/lang/fr/stop_filter_fr.go | 36 + analysis/lang/fr/stop_words_fr.go | 213 + analysis/lang/ga/articles_ga.go | 30 + analysis/lang/ga/elision_ga.go | 40 + analysis/lang/ga/elision_ga_test.go | 55 + analysis/lang/ga/stop_filter_ga.go | 36 + analysis/lang/ga/stop_words_ga.go | 137 + analysis/lang/gl/stop_filter_gl.go | 36 + analysis/lang/gl/stop_words_gl.go | 188 + analysis/lang/hi/analyzer_hi.go | 71 + analysis/lang/hi/analyzer_hi_test.go | 66 + analysis/lang/hi/hindi_normalize.go | 141 + analysis/lang/hi/hindi_normalize_test.go | 251 + analysis/lang/hi/hindi_stemmer_filter.go | 152 + analysis/lang/hi/hindi_stemmer_filter_test.go | 308 ++ analysis/lang/hi/stop_filter_hi.go | 36 + analysis/lang/hi/stop_words_hi.go | 262 + analysis/lang/hr/analyzer_hr.go | 67 + analysis/lang/hr/analyzer_hr_test.go | 97 + analysis/lang/hr/stemmer_hr.go | 156 + analysis/lang/hr/stop_filter_hr.go | 36 + analysis/lang/hr/stop_words_hr.go | 111 + analysis/lang/hr/suffix_transformation_hr.go | 189 + analysis/lang/hu/analyzer_hu.go | 60 + analysis/lang/hu/analyzer_hu_test.go | 70 + analysis/lang/hu/stemmer_hu.go | 52 + analysis/lang/hu/stop_filter_hu.go | 36 + analysis/lang/hu/stop_words_hu.go | 238 + analysis/lang/hy/stop_filter_hy.go | 36 + analysis/lang/hy/stop_words_hy.go | 73 + analysis/lang/id/stop_filter_id.go | 36 + analysis/lang/id/stop_words_id.go | 386 ++ analysis/lang/in/indic_normalize.go | 51 + analysis/lang/in/indic_normalize_test.go | 138 + analysis/lang/in/scripts.go | 296 ++ analysis/lang/it/analyzer_it.go | 65 + analysis/lang/it/analyzer_it_test.go | 96 + analysis/lang/it/articles_it.go | 48 + analysis/lang/it/elision_it.go | 40 + analysis/lang/it/elision_it_test.go | 55 + analysis/lang/it/light_stemmer_it.go | 104 + analysis/lang/it/light_stemmer_it_test.go | 67 + analysis/lang/it/stemmer_it_snowball.go | 52 + analysis/lang/it/stemmer_it_snowball_test.go | 79 + analysis/lang/it/stop_filter_it.go | 36 + analysis/lang/it/stop_words_it.go | 330 ++ analysis/lang/nl/analyzer_nl.go | 60 + analysis/lang/nl/analyzer_nl_test.go | 70 + analysis/lang/nl/stemmer_nl.go | 52 + analysis/lang/nl/stop_filter_nl.go | 36 + analysis/lang/nl/stop_words_nl.go | 146 + analysis/lang/no/analyzer_no.go | 60 + analysis/lang/no/analyzer_no_test.go | 70 + analysis/lang/no/stemmer_no.go | 52 + analysis/lang/no/stop_filter_no.go | 36 + analysis/lang/no/stop_words_no.go | 221 + analysis/lang/pl/analyzer_pl.go | 60 + analysis/lang/pl/analyzer_pl_test.go | 149 + analysis/lang/pl/stemmer_pl.go | 58 + analysis/lang/pl/stemmer_pl_test.go | 67 + analysis/lang/pl/stempel/LICENSE | 202 + analysis/lang/pl/stempel/cell.go | 53 + analysis/lang/pl/stempel/diff.go | 64 + analysis/lang/pl/stempel/diff_test.go | 144 + analysis/lang/pl/stempel/file.go | 71 + analysis/lang/pl/stempel/file_test.go | 91 + analysis/lang/pl/stempel/fuzz.go | 35 + analysis/lang/pl/stempel/javadata/README.md | 3 + analysis/lang/pl/stempel/javadata/fuzz.go | 34 + analysis/lang/pl/stempel/javadata/input.go | 135 + .../lang/pl/stempel/javadata/input_test.go | 249 + analysis/lang/pl/stempel/multi_trie.go | 140 + analysis/lang/pl/stempel/pl/pl_PL.dic.gz | Bin 0 -> 1205814 bytes analysis/lang/pl/stempel/pl/stemmer_20000.tbl | Bin 0 -> 2225192 bytes analysis/lang/pl/stempel/row.go | 80 + analysis/lang/pl/stempel/strenum.go | 48 + analysis/lang/pl/stempel/strenum_test.go | 61 + analysis/lang/pl/stempel/trie.go | 132 + analysis/lang/pl/stop_filter_pl.go | 36 + analysis/lang/pl/stop_words_pl.go | 368 ++ analysis/lang/pt/analyzer_pt.go | 60 + analysis/lang/pt/analyzer_pt_test.go | 70 + analysis/lang/pt/light_stemmer_pt.go | 198 + analysis/lang/pt/light_stemmer_pt_test.go | 404 ++ analysis/lang/pt/stop_filter_pt.go | 36 + analysis/lang/pt/stop_words_pt.go | 280 + analysis/lang/ro/analyzer_ro.go | 60 + analysis/lang/ro/analyzer_ro_test.go | 70 + analysis/lang/ro/stemmer_ro.go | 52 + analysis/lang/ro/stop_filter_ro.go | 36 + analysis/lang/ro/stop_words_ro.go | 260 + analysis/lang/ru/analyzer_ru.go | 60 + analysis/lang/ru/analyzer_ru_test.go | 122 + analysis/lang/ru/stemmer_ru.go | 52 + analysis/lang/ru/stemmer_ru_test.go | 67 + analysis/lang/ru/stop_filter_ru.go | 36 + analysis/lang/ru/stop_words_ru.go | 270 + analysis/lang/sv/analyzer_sv.go | 60 + analysis/lang/sv/analyzer_sv_test.go | 70 + analysis/lang/sv/stemmer_sv.go | 52 + analysis/lang/sv/stop_filter_sv.go | 36 + analysis/lang/sv/stop_words_sv.go | 160 + analysis/lang/tr/analyzer_tr.go | 66 + analysis/lang/tr/analyzer_tr_test.go | 90 + analysis/lang/tr/stemmer_tr.go | 52 + analysis/lang/tr/stemmer_tr_test.go | 115 + analysis/lang/tr/stop_filter_tr.go | 36 + analysis/lang/tr/stop_words_tr.go | 239 + analysis/test_words.txt | 7 + analysis/token/apostrophe/apostrophe.go | 57 + analysis/token/apostrophe/apostrophe_test.go | 99 + analysis/token/camelcase/camelcase.go | 81 + analysis/token/camelcase/camelcase_test.go | 95 + analysis/token/camelcase/parser.go | 109 + analysis/token/camelcase/states.go | 87 + analysis/token/compound/dict.go | 144 + analysis/token/compound/dict_test.go | 187 + analysis/token/edgengram/edgengram.go | 118 + analysis/token/edgengram/edgengram_test.go | 189 + analysis/token/elision/elision.go | 77 + analysis/token/elision/elision_test.go | 85 + analysis/token/hierarchy/hierarchy.go | 95 + analysis/token/hierarchy/hierarchy_test.go | 229 + analysis/token/keyword/keyword.go | 63 + analysis/token/keyword/keyword_test.go | 73 + analysis/token/length/length.go | 80 + analysis/token/length/length_test.go | 99 + analysis/token/lowercase/lowercase.go | 108 + analysis/token/lowercase/lowercase_test.go | 166 + analysis/token/ngram/ngram.go | 116 + analysis/token/ngram/ngram_test.go | 192 + analysis/token/porter/porter.go | 56 + analysis/token/porter/porter_test.go | 115 + analysis/token/reverse/reverse.go | 78 + analysis/token/reverse/reverse_test.go | 184 + analysis/token/shingle/shingle.go | 172 + analysis/token/shingle/shingle_test.go | 416 ++ analysis/token/snowball/snowball.go | 62 + analysis/token/snowball/snowball_test.go | 115 + analysis/token/stop/stop.go | 73 + analysis/token/stop/stop_test.go | 124 + analysis/token/truncate/truncate.go | 62 + analysis/token/truncate/truncate_test.go | 79 + analysis/token/unicodenorm/unicodenorm.go | 82 + .../token/unicodenorm/unicodenorm_test.go | 162 + analysis/token/unique/unique.go | 56 + analysis/token/unique/unique_test.go | 84 + analysis/tokenizer/character/character.go | 76 + .../tokenizer/character/character_test.go | 84 + analysis/tokenizer/exception/exception.go | 144 + .../tokenizer/exception/exception_test.go | 171 + analysis/tokenizer/letter/letter.go | 36 + analysis/tokenizer/regexp/regexp.go | 87 + analysis/tokenizer/regexp/regexp_test.go | 166 + analysis/tokenizer/single/single.go | 52 + analysis/tokenizer/single/single_test.go | 76 + analysis/tokenizer/unicode/unicode.go | 134 + analysis/tokenizer/unicode/unicode_test.go | 202 + analysis/tokenizer/web/web.go | 50 + analysis/tokenizer/web/web_test.go | 148 + analysis/tokenizer/whitespace/whitespace.go | 40 + .../tokenizer/whitespace/whitespace_test.go | 106 + analysis/tokenmap.go | 76 + analysis/tokenmap/custom.go | 65 + analysis/tokenmap_test.go | 43 + analysis/type.go | 120 + analysis/util.go | 92 + analysis/util_test.go | 140 + builder.go | 94 + builder_test.go | 88 + cmd/bleve/.gitignore | 2 + cmd/bleve/cmd/bulk.go | 116 + cmd/bleve/cmd/check.go | 131 + cmd/bleve/cmd/count.go | 40 + cmd/bleve/cmd/create.go | 81 + cmd/bleve/cmd/dictionary.go | 60 + cmd/bleve/cmd/dump.go | 61 + cmd/bleve/cmd/dumpDoc.go | 63 + cmd/bleve/cmd/dumpFields.go | 59 + cmd/bleve/cmd/fields.go | 50 + cmd/bleve/cmd/index.go | 116 + cmd/bleve/cmd/mapping.go | 42 + cmd/bleve/cmd/query.go | 101 + cmd/bleve/cmd/registry.go | 88 + cmd/bleve/cmd/root.go | 93 + cmd/bleve/cmd/scorch.go | 25 + cmd/bleve/cmd/scorch/ascii.go | 59 + cmd/bleve/cmd/scorch/deleted.go | 55 + cmd/bleve/cmd/scorch/info.go | 59 + cmd/bleve/cmd/scorch/internal.go | 61 + cmd/bleve/cmd/scorch/root.go | 70 + cmd/bleve/cmd/scorch/snapshot.go | 64 + cmd/bleve/gendocs.go | 45 + cmd/bleve/main.go | 26 + .../inconshreveable/mousetrap/LICENSE | 13 + .../inconshreveable/mousetrap/trap_others.go | 15 + .../inconshreveable/mousetrap/trap_windows.go | 98 + .../mousetrap/trap_windows_1.4.go | 46 + .../vendor/github.com/spf13/cobra/LICENSE.txt | 174 + .../spf13/cobra/bash_completions.go | 645 +++ .../vendor/github.com/spf13/cobra/cobra.go | 174 + .../vendor/github.com/spf13/cobra/command.go | 1309 +++++ .../github.com/spf13/cobra/command_notwin.go | 5 + .../github.com/spf13/cobra/command_win.go | 26 + .../vendor/github.com/spf13/pflag/LICENSE | 28 + .../vendor/github.com/spf13/pflag/bool.go | 94 + .../github.com/spf13/pflag/bool_slice.go | 147 + .../vendor/github.com/spf13/pflag/count.go | 94 + .../vendor/github.com/spf13/pflag/duration.go | 86 + .../vendor/github.com/spf13/pflag/flag.go | 1063 ++++ .../vendor/github.com/spf13/pflag/float32.go | 88 + .../vendor/github.com/spf13/pflag/float64.go | 84 + .../github.com/spf13/pflag/golangflag.go | 101 + .../vendor/github.com/spf13/pflag/int.go | 84 + .../vendor/github.com/spf13/pflag/int32.go | 88 + .../vendor/github.com/spf13/pflag/int64.go | 84 + .../vendor/github.com/spf13/pflag/int8.go | 88 + .../github.com/spf13/pflag/int_slice.go | 128 + cmd/bleve/vendor/github.com/spf13/pflag/ip.go | 94 + .../vendor/github.com/spf13/pflag/ip_slice.go | 148 + .../vendor/github.com/spf13/pflag/ipmask.go | 122 + .../vendor/github.com/spf13/pflag/ipnet.go | 98 + .../vendor/github.com/spf13/pflag/string.go | 80 + .../github.com/spf13/pflag/string_array.go | 103 + .../github.com/spf13/pflag/string_slice.go | 129 + .../vendor/github.com/spf13/pflag/uint.go | 88 + .../vendor/github.com/spf13/pflag/uint16.go | 88 + .../vendor/github.com/spf13/pflag/uint32.go | 88 + .../vendor/github.com/spf13/pflag/uint64.go | 88 + .../vendor/github.com/spf13/pflag/uint8.go | 88 + .../github.com/spf13/pflag/uint_slice.go | 126 + cmd/bleve/vendor/manifest | 29 + config.go | 95 + config/README.md | 11 + config/config.go | 124 + config_app.go | 24 + config_disk.go | 26 + data/test/sample-data.json | 10 + doc.go | 37 + docs/bleve.png | Bin 0 -> 6727 bytes docs/geo.md | 3 + docs/scoring.md | 88 + docs/sort_facet.md | 786 +++ .../indexSizeVsNumDocs.png | Bin 0 -> 30918 bytes .../queryTimevsNumDocs.png | Bin 0 -> 32162 bytes docs/synonyms.md | 180 + docs/vectors.md | 149 + document/document.go | 159 + document/document_test.go | 73 + document/field.go | 45 + document/field_boolean.go | 142 + document/field_composite.go | 138 + document/field_datetime.go | 202 + document/field_geopoint.go | 199 + document/field_geopoint_test.go | 16 + document/field_geoshape.go | 265 + document/field_ip.go | 137 + document/field_ip_test.go | 38 + document/field_numeric.go | 165 + document/field_numeric_test.go | 32 + document/field_synonym.go | 149 + document/field_text.go | 162 + document/field_vector.go | 146 + document/field_vector_base64.go | 163 + document/field_vector_base64_test.go | 112 + error.go | 54 + examples_test.go | 468 ++ geo/README.md | 283 + geo/benchmark_geohash_test.go | 43 + geo/geo.go | 210 + geo/geo_dist.go | 98 + geo/geo_dist_test.go | 120 + geo/geo_s2plugin_impl.go | 463 ++ geo/geo_test.go | 183 + geo/geohash.go | 111 + geo/geohash_test.go | 64 + geo/parse.go | 465 ++ geo/parse_test.go | 478 ++ geo/sloppy.go | 59 + geo/versus_test.go | 4156 +++++++++++++++ go.mod | 46 + go.sum | 122 + index.go | 388 ++ index/scorch/README.md | 367 ++ index/scorch/builder.go | 332 ++ index/scorch/builder_test.go | 159 + index/scorch/empty.go | 41 + index/scorch/event.go | 77 + index/scorch/event_test.go | 79 + index/scorch/field_dict_test.go | 186 + index/scorch/int.go | 92 + index/scorch/int_test.go | 96 + index/scorch/introducer.go | 515 ++ index/scorch/merge.go | 637 +++ index/scorch/merge_test.go | 157 + index/scorch/mergeplan/merge_plan.go | 454 ++ index/scorch/mergeplan/merge_plan_test.go | 721 +++ index/scorch/mergeplan/sort.go | 28 + index/scorch/optimize.go | 397 ++ index/scorch/optimize_knn.go | 207 + index/scorch/persister.go | 1445 ++++++ index/scorch/reader_test.go | 697 +++ index/scorch/regexp.go | 63 + index/scorch/regexp_test.go | 57 + index/scorch/rollback.go | 216 + index/scorch/rollback_test.go | 623 +++ index/scorch/scorch.go | 942 ++++ index/scorch/scorch_test.go | 2812 ++++++++++ index/scorch/segment_plugin.go | 144 + index/scorch/snapshot_index.go | 1163 +++++ index/scorch/snapshot_index_dict.go | 119 + index/scorch/snapshot_index_doc.go | 80 + index/scorch/snapshot_index_str.go | 79 + index/scorch/snapshot_index_test.go | 90 + index/scorch/snapshot_index_tfr.go | 216 + index/scorch/snapshot_index_thes.go | 107 + index/scorch/snapshot_index_vr.go | 165 + index/scorch/snapshot_segment.go | 340 ++ index/scorch/snapshot_vector_index.go | 85 + index/scorch/stats.go | 160 + index/scorch/unadorned.go | 182 + index/upsidedown/analysis.go | 129 + index/upsidedown/analysis_test.go | 115 + index/upsidedown/benchmark_all.sh | 8 + index/upsidedown/benchmark_boltdb_test.go | 75 + index/upsidedown/benchmark_common_test.go | 149 + index/upsidedown/benchmark_gtreap_test.go | 71 + index/upsidedown/benchmark_null_test.go | 71 + index/upsidedown/dump.go | 174 + index/upsidedown/dump_test.go | 155 + index/upsidedown/field_cache.go | 88 + index/upsidedown/field_dict.go | 86 + index/upsidedown/field_dict_test.go | 183 + index/upsidedown/index_reader.go | 228 + index/upsidedown/reader.go | 376 ++ index/upsidedown/reader_test.go | 548 ++ index/upsidedown/row.go | 1144 +++++ index/upsidedown/row_merge.go | 76 + index/upsidedown/row_merge_test.go | 57 + index/upsidedown/row_test.go | 382 ++ index/upsidedown/stats.go | 55 + index/upsidedown/store/boltdb/iterator.go | 85 + index/upsidedown/store/boltdb/reader.go | 73 + index/upsidedown/store/boltdb/stats.go | 28 + index/upsidedown/store/boltdb/store.go | 184 + index/upsidedown/store/boltdb/store_test.go | 148 + index/upsidedown/store/boltdb/writer.go | 95 + index/upsidedown/store/goleveldb/batch.go | 50 + index/upsidedown/store/goleveldb/config.go | 66 + index/upsidedown/store/goleveldb/iterator.go | 54 + index/upsidedown/store/goleveldb/reader.go | 68 + index/upsidedown/store/goleveldb/store.go | 152 + .../upsidedown/store/goleveldb/store_test.go | 99 + index/upsidedown/store/goleveldb/writer.go | 68 + index/upsidedown/store/gtreap/iterator.go | 152 + index/upsidedown/store/gtreap/reader.go | 66 + index/upsidedown/store/gtreap/store.go | 85 + index/upsidedown/store/gtreap/store_test.go | 93 + index/upsidedown/store/gtreap/writer.go | 76 + index/upsidedown/store/metrics/batch.go | 46 + index/upsidedown/store/metrics/iterator.go | 58 + .../upsidedown/store/metrics/metrics_test.go | 141 + index/upsidedown/store/metrics/reader.go | 64 + index/upsidedown/store/metrics/stats.go | 50 + index/upsidedown/store/metrics/store.go | 277 + index/upsidedown/store/metrics/store_test.go | 95 + index/upsidedown/store/metrics/util.go | 135 + index/upsidedown/store/metrics/writer.go | 60 + index/upsidedown/store/moss/batch.go | 87 + index/upsidedown/store/moss/iterator.go | 87 + index/upsidedown/store/moss/lower.go | 571 +++ index/upsidedown/store/moss/lower_test.go | 103 + index/upsidedown/store/moss/reader.go | 97 + index/upsidedown/store/moss/stats.go | 58 + index/upsidedown/store/moss/store.go | 231 + index/upsidedown/store/moss/store_test.go | 91 + index/upsidedown/store/moss/writer.go | 97 + index/upsidedown/store/null/null.go | 121 + index/upsidedown/store/null/null_test.go | 92 + index/upsidedown/upsidedown.go | 1079 ++++ index/upsidedown/upsidedown.pb.go | 690 +++ index/upsidedown/upsidedown.proto | 14 + index/upsidedown/upsidedown_test.go | 1529 ++++++ index_alias.go | 37 + index_alias_impl.go | 1067 ++++ index_alias_impl_test.go | 1355 +++++ index_impl.go | 1288 +++++ index_meta.go | 115 + index_meta_test.go | 59 + index_stats.go | 75 + index_test.go | 3247 ++++++++++++ mapping.go | 79 + mapping/analysis.go | 107 + mapping/document.go | 646 +++ mapping/examples_test.go | 61 + mapping/field.go | 492 ++ mapping/index.go | 573 +++ mapping/mapping.go | 76 + mapping/mapping_no_vectors.go | 44 + mapping/mapping_test.go | 1260 +++++ mapping/mapping_vectors.go | 272 + mapping/mapping_vectors_test.go | 334 ++ mapping/reflect.go | 92 + mapping/reflect_test.go | 47 + mapping/synonym.go | 71 + mapping_vector.go | 28 + numeric/bin.go | 43 + numeric/bin_test.go | 27 + numeric/float.go | 34 + numeric/float_test.go | 64 + numeric/prefix_coded.go | 111 + numeric/prefix_coded_test.go | 158 + pre_search.go | 170 + query.go | 290 ++ query_bench_test.go | 393 ++ registry/analyzer.go | 90 + registry/cache.go | 87 + registry/char_filter.go | 90 + registry/datetime_parser.go | 90 + registry/fragment_formatter.go | 90 + registry/fragmenter.go | 90 + registry/highlighter.go | 90 + registry/index_type.go | 46 + registry/registry.go | 195 + registry/store.go | 52 + registry/synonym_source.go | 86 + registry/token_filter.go | 90 + registry/token_maps.go | 90 + registry/tokenizer.go | 90 + scripts/build_children.sh | 6 + scripts/merge-coverprofile.go | 62 + scripts/old_build_script.txt | 29 + scripts/project-code-coverage.sh | 52 + search.go | 609 +++ search/collector.go | 58 + search/collector/bench_test.go | 50 + search/collector/eligible.go | 172 + search/collector/heap.go | 99 + search/collector/knn.go | 262 + search/collector/list.go | 96 + search/collector/search_test.go | 187 + search/collector/slice.go | 83 + search/collector/topn.go | 558 ++ search/collector/topn_test.go | 868 ++++ search/explanation.go | 56 + search/facet/benchmark_data.txt | 2909 +++++++++++ search/facet/facet_builder_datetime.go | 163 + search/facet/facet_builder_numeric.go | 157 + search/facet/facet_builder_numeric_test.go | 64 + search/facet/facet_builder_terms.go | 115 + search/facet/facet_builder_terms_test.go | 72 + search/facets_builder.go | 411 ++ search/facets_builder_test.go | 424 ++ search/highlight/format/ansi/ansi.go | 112 + search/highlight/format/html/html.go | 94 + search/highlight/format/html/html_test.go | 120 + search/highlight/format/plain/plain.go | 92 + search/highlight/format/plain/plain_test.go | 80 + search/highlight/fragmenter/simple/simple.go | 156 + .../fragmenter/simple/simple_test.go | 311 ++ search/highlight/highlighter.go | 64 + search/highlight/highlighter/ansi/ansi.go | 53 + search/highlight/highlighter/html/html.go | 53 + .../simple/fragment_scorer_simple.go | 49 + .../simple/fragment_scorer_simple_test.go | 82 + .../highlighter/simple/highlighter_simple.go | 225 + .../simple/highlighter_simple_test.go | 169 + search/highlight/term_locations.go | 105 + search/highlight/term_locations_test.go | 512 ++ search/levenshtein.go | 118 + search/levenshtein_test.go | 126 + search/pool.go | 91 + search/pool_test.go | 70 + search/query/bool_field.go | 66 + search/query/boolean.go | 259 + search/query/boost.go | 33 + search/query/conjunction.go | 112 + search/query/date_range.go | 192 + search/query/date_range_string.go | 176 + search/query/date_range_test.go | 132 + search/query/disjunction.go | 134 + search/query/docid.go | 51 + search/query/fuzzy.go | 134 + search/query/geo_boundingbox.go | 119 + search/query/geo_boundingpolygon.go | 97 + search/query/geo_distance.go | 103 + search/query/geo_shape.go | 138 + search/query/ip_range.go | 85 + search/query/knn.go | 95 + search/query/match.go | 236 + search/query/match_all.go | 56 + search/query/match_none.go | 56 + search/query/match_phrase.go | 176 + search/query/match_phrase_test.go | 101 + search/query/multi_phrase.go | 130 + search/query/numeric_range.go | 89 + search/query/phrase.go | 127 + search/query/prefix.go | 64 + search/query/query.go | 783 +++ search/query/query_string.go | 69 + search/query/query_string.y | 338 ++ search/query/query_string.y.go | 833 +++ search/query/query_string_lex.go | 329 ++ search/query/query_string_lex_test.go | 1230 +++++ search/query/query_string_parser.go | 85 + search/query/query_string_parser_test.go | 954 ++++ search/query/query_test.go | 1042 ++++ search/query/regexp.go | 79 + search/query/term.go | 63 + search/query/term_range.go | 96 + search/query/wildcard.go | 94 + search/scorer/scorer_conjunction.go | 72 + search/scorer/scorer_constant.go | 132 + search/scorer/scorer_constant_test.go | 131 + search/scorer/scorer_disjunction.go | 123 + search/scorer/scorer_knn.go | 157 + search/scorer/scorer_knn_test.go | 181 + search/scorer/scorer_term.go | 276 + search/scorer/scorer_term_test.go | 260 + search/scorer/sqrt_cache.go | 30 + search/search.go | 396 ++ search/search_test.go | 94 + search/searcher/base_test.go | 117 + search/searcher/geoshape_contains_test.go | 1006 ++++ search/searcher/geoshape_intersects_test.go | 1785 +++++++ search/searcher/geoshape_within_test.go | 1351 +++++ search/searcher/optimize_knn.go | 53 + search/searcher/optimize_no_knn.go | 31 + search/searcher/ordered_searchers_list.go | 55 + search/searcher/search_boolean.go | 451 ++ search/searcher/search_boolean_test.go | 382 ++ search/searcher/search_conjunction.go | 285 ++ search/searcher/search_conjunction_test.go | 438 ++ search/searcher/search_disjunction.go | 131 + search/searcher/search_disjunction_heap.go | 367 ++ search/searcher/search_disjunction_slice.go | 334 ++ search/searcher/search_disjunction_test.go | 223 + search/searcher/search_docid.go | 110 + search/searcher/search_docid_test.go | 146 + search/searcher/search_filter.go | 104 + search/searcher/search_fuzzy.go | 250 + search/searcher/search_fuzzy_test.go | 156 + search/searcher/search_geoboundingbox.go | 306 ++ search/searcher/search_geoboundingbox_test.go | 317 ++ search/searcher/search_geopointdistance.go | 151 + .../searcher/search_geopointdistance_test.go | 157 + search/searcher/search_geopolygon.go | 149 + search/searcher/search_geopolygon_test.go | 409 ++ search/searcher/search_geoshape.go | 135 + .../searcher/search_geoshape_circle_test.go | 541 ++ .../searcher/search_geoshape_envelope_test.go | 520 ++ ...search_geoshape_geometrycollection_test.go | 692 +++ .../search_geoshape_linestring_test.go | 687 +++ .../searcher/search_geoshape_points_test.go | 600 +++ .../searcher/search_geoshape_polygon_test.go | 1213 +++++ search/searcher/search_ip_range.go | 68 + search/searcher/search_ip_range_test.go | 52 + search/searcher/search_knn.go | 145 + search/searcher/search_match_all.go | 123 + search/searcher/search_match_all_test.go | 146 + search/searcher/search_match_none.go | 76 + search/searcher/search_match_none_test.go | 85 + search/searcher/search_multi_term.go | 268 + search/searcher/search_numeric_range.go | 258 + search/searcher/search_numeric_range_test.go | 60 + search/searcher/search_phrase.go | 554 ++ search/searcher/search_phrase_test.go | 818 +++ search/searcher/search_regexp.go | 169 + search/searcher/search_regexp_test.go | 171 + search/searcher/search_term.go | 282 + search/searcher/search_term_prefix.go | 86 + search/searcher/search_term_range.go | 92 + search/searcher/search_term_range_test.go | 285 ++ search/searcher/search_term_test.go | 185 + search/sort.go | 764 +++ search/sort_test.go | 338 ++ search/util.go | 235 + search/util_test.go | 91 + search_knn.go | 610 +++ search_knn_test.go | 1703 ++++++ search_no_knn.go | 209 + search_test.go | 4544 +++++++++++++++++ size/sizes.go | 59 + test/integration.go | 27 + test/integration_test.go | 279 + test/ip_field_test.go | 272 + test/knn/knn_dataset_queries.zip | Bin 0 -> 149642 bytes test/tests/alias/datasets/shard0/a.json | 3 + test/tests/alias/datasets/shard0/c.json | 3 + test/tests/alias/datasets/shard1/b.json | 3 + test/tests/alias/datasets/shard1/d.json | 3 + test/tests/alias/mapping.json | 3 + test/tests/alias/searches.json | 76 + test/tests/basic/data/a.json | 7 + test/tests/basic/data/b.json | 7 + test/tests/basic/data/c.json | 7 + test/tests/basic/data/d.json | 7 + test/tests/basic/mapping.json | 27 + test/tests/basic/searches.json | 883 ++++ test/tests/employee/data/emp10508560.json | 44 + test/tests/employee/mapping.json | 1 + test/tests/employee/searches.json | 41 + test/tests/facet/data/a.json | 6 + test/tests/facet/data/b.json | 6 + test/tests/facet/data/c.json | 6 + test/tests/facet/data/d.json | 6 + test/tests/facet/data/e.json | 6 + test/tests/facet/data/f.json | 6 + test/tests/facet/data/g.json | 6 + test/tests/facet/data/h.json | 6 + test/tests/facet/data/i.json | 6 + test/tests/facet/data/j.json | 6 + test/tests/facet/mapping.json | 1 + test/tests/facet/searches.json | 144 + .../fosdem/data/3311@FOSDEM15@fosdem.org.json | 4 + .../fosdem/data/3492@FOSDEM15@fosdem.org.json | 4 + .../fosdem/data/3496@FOSDEM15@fosdem.org.json | 4 + .../fosdem/data/3505@FOSDEM15@fosdem.org.json | 4 + .../fosdem/data/3507@FOSDEM15@fosdem.org.json | 4 + test/tests/fosdem/mapping.json | 76 + test/tests/fosdem/searches.json | 109 + test/tests/geo/data/amoeba_brewery.json | 1 + test/tests/geo/data/brewpub_on_the_green.json | 1 + .../data/capital_city_brewing_company.json | 1 + test/tests/geo/data/communiti_brewery.json | 1 + .../geo/data/firehouse_grill_brewery.json | 1 + .../geo/data/hook_ladder_brewing_company.json | 1 + test/tests/geo/data/jack_s_brewing.json | 1 + test/tests/geo/data/social_brewery.json | 1 + .../data/sweet_water_tavern_and_brewery.json | 1 + test/tests/geo/mapping.json | 36 + test/tests/geo/searches.json | 326 ++ .../geoshapes/data/circle_halairport.json | 14 + .../data/envelope_brockwell_park.json | 19 + .../data/geometrycollection_tvm.json | 165 + .../data/linestring_putney_bridge.json | 19 + .../multilinestring_old_airport_road.json | 48 + .../data/multipoint_blr_stadiums.json | 23 + .../data/multipolygon_london_parks.json | 87 + .../data/point_museum_of_london.json | 13 + .../geoshapes/data/polygon_cubbonpark.json | 45 + test/tests/geoshapes/mapping.json | 36 + test/tests/geoshapes/searches.json | 1500 ++++++ test/tests/phrase/data/a.json | 3 + test/tests/phrase/data/b.json | 3 + test/tests/phrase/mapping.json | 23 + test/tests/phrase/searches.json | 417 ++ test/tests/sort/data/a.json | 8 + test/tests/sort/data/b.json | 8 + test/tests/sort/data/c.json | 8 + test/tests/sort/data/d.json | 7 + test/tests/sort/data/e.json | 7 + test/tests/sort/data/f.json | 7 + test/tests/sort/mapping.json | 3 + test/tests/sort/searches.json | 554 ++ test/versus_score_test.go | 140 + test/versus_test.go | 519 ++ util/extract.go | 66 + util/json.go | 25 + 783 files changed, 150650 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 analysis/analyzer/custom/custom.go create mode 100644 analysis/analyzer/keyword/keyword.go create mode 100644 analysis/analyzer/simple/simple.go create mode 100644 analysis/analyzer/standard/standard.go create mode 100644 analysis/analyzer/web/web.go create mode 100644 analysis/benchmark_test.go create mode 100644 analysis/char/asciifolding/asciifolding.go create mode 100644 analysis/char/asciifolding/asciifolding_test.go create mode 100644 analysis/char/html/html.go create mode 100644 analysis/char/regexp/regexp.go create mode 100644 analysis/char/regexp/regexp_test.go create mode 100644 analysis/char/zerowidthnonjoiner/zerowidthnonjoiner.go create mode 100644 analysis/datetime/flexible/flexible.go create mode 100644 analysis/datetime/flexible/flexible_test.go create mode 100644 analysis/datetime/iso/iso.go create mode 100644 analysis/datetime/iso/iso_test.go create mode 100644 analysis/datetime/optional/optional.go create mode 100644 analysis/datetime/percent/percent.go create mode 100644 analysis/datetime/percent/percent_test.go create mode 100644 analysis/datetime/sanitized/sanitized.go create mode 100644 analysis/datetime/sanitized/sanitized_test.go create mode 100644 analysis/datetime/timestamp/microseconds/microseconds.go create mode 100644 analysis/datetime/timestamp/milliseconds/milliseconds.go create mode 100644 analysis/datetime/timestamp/nanoseconds/nanoseconds.go create mode 100644 analysis/datetime/timestamp/seconds/seconds.go create mode 100644 analysis/freq.go create mode 100644 analysis/freq_test.go create mode 100644 analysis/lang/ar/analyzer_ar.go create mode 100644 analysis/lang/ar/analyzer_ar_test.go create mode 100644 analysis/lang/ar/arabic_normalize.go create mode 100644 analysis/lang/ar/arabic_normalize_test.go create mode 100644 analysis/lang/ar/stemmer_ar.go create mode 100644 analysis/lang/ar/stemmer_ar_test.go create mode 100644 analysis/lang/ar/stop_filter_ar.go create mode 100644 analysis/lang/ar/stop_words_ar.go create mode 100644 analysis/lang/bg/stop_filter_bg.go create mode 100644 analysis/lang/bg/stop_words_bg.go create mode 100644 analysis/lang/ca/articles_ca.go create mode 100644 analysis/lang/ca/elision_ca.go create mode 100644 analysis/lang/ca/elision_ca_test.go create mode 100644 analysis/lang/ca/stop_filter_ca.go create mode 100644 analysis/lang/ca/stop_words_ca.go create mode 100644 analysis/lang/cjk/analyzer_cjk.go create mode 100644 analysis/lang/cjk/analyzer_cjk_test.go create mode 100644 analysis/lang/cjk/cjk_bigram.go create mode 100644 analysis/lang/cjk/cjk_bigram_test.go create mode 100644 analysis/lang/cjk/cjk_width.go create mode 100644 analysis/lang/cjk/cjk_width_test.go create mode 100644 analysis/lang/ckb/analyzer_ckb.go create mode 100644 analysis/lang/ckb/analyzer_ckb_test.go create mode 100644 analysis/lang/ckb/sorani_normalize.go create mode 100644 analysis/lang/ckb/sorani_normalize_test.go create mode 100644 analysis/lang/ckb/sorani_stemmer_filter.go create mode 100644 analysis/lang/ckb/sorani_stemmer_filter_test.go create mode 100644 analysis/lang/ckb/stop_filter_ckb.go create mode 100644 analysis/lang/ckb/stop_words_ckb.go create mode 100644 analysis/lang/cs/stop_filter_cs.go create mode 100644 analysis/lang/cs/stop_words_cs.go create mode 100644 analysis/lang/da/analyzer_da.go create mode 100644 analysis/lang/da/analyzer_da_test.go create mode 100644 analysis/lang/da/stemmer_da.go create mode 100644 analysis/lang/da/stop_filter_da.go create mode 100644 analysis/lang/da/stop_words_da.go create mode 100644 analysis/lang/de/analyzer_de.go create mode 100644 analysis/lang/de/analyzer_de_test.go create mode 100644 analysis/lang/de/german_normalize.go create mode 100644 analysis/lang/de/german_normalize_test.go create mode 100644 analysis/lang/de/light_stemmer_de.go create mode 100644 analysis/lang/de/stemmer_de_snowball.go create mode 100644 analysis/lang/de/stemmer_de_test.go create mode 100644 analysis/lang/de/stop_filter_de.go create mode 100644 analysis/lang/de/stop_words_de.go create mode 100644 analysis/lang/el/stop_filter_el.go create mode 100644 analysis/lang/el/stop_words_el.go create mode 100644 analysis/lang/en/analyzer_en.go create mode 100644 analysis/lang/en/analyzer_en_test.go create mode 100644 analysis/lang/en/plural_stemmer.go create mode 100644 analysis/lang/en/plural_stemmer_test.go create mode 100644 analysis/lang/en/possessive_filter_en.go create mode 100644 analysis/lang/en/possessive_filter_en_test.go create mode 100644 analysis/lang/en/stemmer_en_snowball.go create mode 100644 analysis/lang/en/stemmer_en_test.go create mode 100644 analysis/lang/en/stop_filter_en.go create mode 100644 analysis/lang/en/stop_words_en.go create mode 100644 analysis/lang/es/analyzer_es.go create mode 100644 analysis/lang/es/analyzer_es_test.go create mode 100644 analysis/lang/es/light_stemmer_es.go create mode 100644 analysis/lang/es/spanish_normalize.go create mode 100644 analysis/lang/es/spanish_normalize_test.go create mode 100644 analysis/lang/es/stemmer_es_snowball.go create mode 100644 analysis/lang/es/stemmer_es_snowball_test.go create mode 100644 analysis/lang/es/stop_filter_es.go create mode 100644 analysis/lang/es/stop_words_es.go create mode 100644 analysis/lang/eu/stop_filter_eu.go create mode 100644 analysis/lang/eu/stop_words_eu.go create mode 100644 analysis/lang/fa/analyzer_fa.go create mode 100644 analysis/lang/fa/analyzer_fa_test.go create mode 100644 analysis/lang/fa/persian_normalize.go create mode 100644 analysis/lang/fa/persian_normalize_test.go create mode 100644 analysis/lang/fa/stop_filter_fa.go create mode 100644 analysis/lang/fa/stop_words_fa.go create mode 100644 analysis/lang/fi/analyzer_fi.go create mode 100644 analysis/lang/fi/analyzer_fi_test.go create mode 100644 analysis/lang/fi/stemmer_fi.go create mode 100644 analysis/lang/fi/stop_filter_fi.go create mode 100644 analysis/lang/fi/stop_words_fi.go create mode 100644 analysis/lang/fr/analyzer_fr.go create mode 100644 analysis/lang/fr/analyzer_fr_test.go create mode 100644 analysis/lang/fr/articles_fr.go create mode 100644 analysis/lang/fr/elision_fr.go create mode 100644 analysis/lang/fr/elision_fr_test.go create mode 100644 analysis/lang/fr/light_stemmer_fr.go create mode 100644 analysis/lang/fr/light_stemmer_fr_test.go create mode 100644 analysis/lang/fr/minimal_stemmer_fr.go create mode 100644 analysis/lang/fr/minimal_stemmer_fr_test.go create mode 100644 analysis/lang/fr/stemmer_fr_snowball.go create mode 100644 analysis/lang/fr/stemmer_fr_snowball_test.go create mode 100644 analysis/lang/fr/stop_filter_fr.go create mode 100644 analysis/lang/fr/stop_words_fr.go create mode 100644 analysis/lang/ga/articles_ga.go create mode 100644 analysis/lang/ga/elision_ga.go create mode 100644 analysis/lang/ga/elision_ga_test.go create mode 100644 analysis/lang/ga/stop_filter_ga.go create mode 100644 analysis/lang/ga/stop_words_ga.go create mode 100644 analysis/lang/gl/stop_filter_gl.go create mode 100644 analysis/lang/gl/stop_words_gl.go create mode 100644 analysis/lang/hi/analyzer_hi.go create mode 100644 analysis/lang/hi/analyzer_hi_test.go create mode 100644 analysis/lang/hi/hindi_normalize.go create mode 100644 analysis/lang/hi/hindi_normalize_test.go create mode 100644 analysis/lang/hi/hindi_stemmer_filter.go create mode 100644 analysis/lang/hi/hindi_stemmer_filter_test.go create mode 100644 analysis/lang/hi/stop_filter_hi.go create mode 100644 analysis/lang/hi/stop_words_hi.go create mode 100644 analysis/lang/hr/analyzer_hr.go create mode 100644 analysis/lang/hr/analyzer_hr_test.go create mode 100644 analysis/lang/hr/stemmer_hr.go create mode 100644 analysis/lang/hr/stop_filter_hr.go create mode 100644 analysis/lang/hr/stop_words_hr.go create mode 100644 analysis/lang/hr/suffix_transformation_hr.go create mode 100644 analysis/lang/hu/analyzer_hu.go create mode 100644 analysis/lang/hu/analyzer_hu_test.go create mode 100644 analysis/lang/hu/stemmer_hu.go create mode 100644 analysis/lang/hu/stop_filter_hu.go create mode 100644 analysis/lang/hu/stop_words_hu.go create mode 100644 analysis/lang/hy/stop_filter_hy.go create mode 100644 analysis/lang/hy/stop_words_hy.go create mode 100644 analysis/lang/id/stop_filter_id.go create mode 100644 analysis/lang/id/stop_words_id.go create mode 100644 analysis/lang/in/indic_normalize.go create mode 100644 analysis/lang/in/indic_normalize_test.go create mode 100644 analysis/lang/in/scripts.go create mode 100644 analysis/lang/it/analyzer_it.go create mode 100644 analysis/lang/it/analyzer_it_test.go create mode 100644 analysis/lang/it/articles_it.go create mode 100644 analysis/lang/it/elision_it.go create mode 100644 analysis/lang/it/elision_it_test.go create mode 100644 analysis/lang/it/light_stemmer_it.go create mode 100644 analysis/lang/it/light_stemmer_it_test.go create mode 100644 analysis/lang/it/stemmer_it_snowball.go create mode 100644 analysis/lang/it/stemmer_it_snowball_test.go create mode 100644 analysis/lang/it/stop_filter_it.go create mode 100644 analysis/lang/it/stop_words_it.go create mode 100644 analysis/lang/nl/analyzer_nl.go create mode 100644 analysis/lang/nl/analyzer_nl_test.go create mode 100644 analysis/lang/nl/stemmer_nl.go create mode 100644 analysis/lang/nl/stop_filter_nl.go create mode 100644 analysis/lang/nl/stop_words_nl.go create mode 100644 analysis/lang/no/analyzer_no.go create mode 100644 analysis/lang/no/analyzer_no_test.go create mode 100644 analysis/lang/no/stemmer_no.go create mode 100644 analysis/lang/no/stop_filter_no.go create mode 100644 analysis/lang/no/stop_words_no.go create mode 100644 analysis/lang/pl/analyzer_pl.go create mode 100644 analysis/lang/pl/analyzer_pl_test.go create mode 100644 analysis/lang/pl/stemmer_pl.go create mode 100644 analysis/lang/pl/stemmer_pl_test.go create mode 100644 analysis/lang/pl/stempel/LICENSE create mode 100644 analysis/lang/pl/stempel/cell.go create mode 100644 analysis/lang/pl/stempel/diff.go create mode 100644 analysis/lang/pl/stempel/diff_test.go create mode 100644 analysis/lang/pl/stempel/file.go create mode 100644 analysis/lang/pl/stempel/file_test.go create mode 100644 analysis/lang/pl/stempel/fuzz.go create mode 100644 analysis/lang/pl/stempel/javadata/README.md create mode 100644 analysis/lang/pl/stempel/javadata/fuzz.go create mode 100644 analysis/lang/pl/stempel/javadata/input.go create mode 100644 analysis/lang/pl/stempel/javadata/input_test.go create mode 100644 analysis/lang/pl/stempel/multi_trie.go create mode 100644 analysis/lang/pl/stempel/pl/pl_PL.dic.gz create mode 100644 analysis/lang/pl/stempel/pl/stemmer_20000.tbl create mode 100644 analysis/lang/pl/stempel/row.go create mode 100644 analysis/lang/pl/stempel/strenum.go create mode 100644 analysis/lang/pl/stempel/strenum_test.go create mode 100644 analysis/lang/pl/stempel/trie.go create mode 100644 analysis/lang/pl/stop_filter_pl.go create mode 100644 analysis/lang/pl/stop_words_pl.go create mode 100644 analysis/lang/pt/analyzer_pt.go create mode 100644 analysis/lang/pt/analyzer_pt_test.go create mode 100644 analysis/lang/pt/light_stemmer_pt.go create mode 100644 analysis/lang/pt/light_stemmer_pt_test.go create mode 100644 analysis/lang/pt/stop_filter_pt.go create mode 100644 analysis/lang/pt/stop_words_pt.go create mode 100644 analysis/lang/ro/analyzer_ro.go create mode 100644 analysis/lang/ro/analyzer_ro_test.go create mode 100644 analysis/lang/ro/stemmer_ro.go create mode 100644 analysis/lang/ro/stop_filter_ro.go create mode 100644 analysis/lang/ro/stop_words_ro.go create mode 100644 analysis/lang/ru/analyzer_ru.go create mode 100644 analysis/lang/ru/analyzer_ru_test.go create mode 100644 analysis/lang/ru/stemmer_ru.go create mode 100644 analysis/lang/ru/stemmer_ru_test.go create mode 100644 analysis/lang/ru/stop_filter_ru.go create mode 100644 analysis/lang/ru/stop_words_ru.go create mode 100644 analysis/lang/sv/analyzer_sv.go create mode 100644 analysis/lang/sv/analyzer_sv_test.go create mode 100644 analysis/lang/sv/stemmer_sv.go create mode 100644 analysis/lang/sv/stop_filter_sv.go create mode 100644 analysis/lang/sv/stop_words_sv.go create mode 100644 analysis/lang/tr/analyzer_tr.go create mode 100644 analysis/lang/tr/analyzer_tr_test.go create mode 100644 analysis/lang/tr/stemmer_tr.go create mode 100644 analysis/lang/tr/stemmer_tr_test.go create mode 100644 analysis/lang/tr/stop_filter_tr.go create mode 100644 analysis/lang/tr/stop_words_tr.go create mode 100644 analysis/test_words.txt create mode 100644 analysis/token/apostrophe/apostrophe.go create mode 100644 analysis/token/apostrophe/apostrophe_test.go create mode 100644 analysis/token/camelcase/camelcase.go create mode 100644 analysis/token/camelcase/camelcase_test.go create mode 100644 analysis/token/camelcase/parser.go create mode 100644 analysis/token/camelcase/states.go create mode 100644 analysis/token/compound/dict.go create mode 100644 analysis/token/compound/dict_test.go create mode 100644 analysis/token/edgengram/edgengram.go create mode 100644 analysis/token/edgengram/edgengram_test.go create mode 100644 analysis/token/elision/elision.go create mode 100644 analysis/token/elision/elision_test.go create mode 100644 analysis/token/hierarchy/hierarchy.go create mode 100644 analysis/token/hierarchy/hierarchy_test.go create mode 100644 analysis/token/keyword/keyword.go create mode 100644 analysis/token/keyword/keyword_test.go create mode 100644 analysis/token/length/length.go create mode 100644 analysis/token/length/length_test.go create mode 100644 analysis/token/lowercase/lowercase.go create mode 100644 analysis/token/lowercase/lowercase_test.go create mode 100644 analysis/token/ngram/ngram.go create mode 100644 analysis/token/ngram/ngram_test.go create mode 100644 analysis/token/porter/porter.go create mode 100644 analysis/token/porter/porter_test.go create mode 100644 analysis/token/reverse/reverse.go create mode 100644 analysis/token/reverse/reverse_test.go create mode 100644 analysis/token/shingle/shingle.go create mode 100644 analysis/token/shingle/shingle_test.go create mode 100644 analysis/token/snowball/snowball.go create mode 100644 analysis/token/snowball/snowball_test.go create mode 100644 analysis/token/stop/stop.go create mode 100644 analysis/token/stop/stop_test.go create mode 100644 analysis/token/truncate/truncate.go create mode 100644 analysis/token/truncate/truncate_test.go create mode 100644 analysis/token/unicodenorm/unicodenorm.go create mode 100644 analysis/token/unicodenorm/unicodenorm_test.go create mode 100644 analysis/token/unique/unique.go create mode 100644 analysis/token/unique/unique_test.go create mode 100644 analysis/tokenizer/character/character.go create mode 100644 analysis/tokenizer/character/character_test.go create mode 100644 analysis/tokenizer/exception/exception.go create mode 100644 analysis/tokenizer/exception/exception_test.go create mode 100644 analysis/tokenizer/letter/letter.go create mode 100644 analysis/tokenizer/regexp/regexp.go create mode 100644 analysis/tokenizer/regexp/regexp_test.go create mode 100644 analysis/tokenizer/single/single.go create mode 100644 analysis/tokenizer/single/single_test.go create mode 100644 analysis/tokenizer/unicode/unicode.go create mode 100644 analysis/tokenizer/unicode/unicode_test.go create mode 100644 analysis/tokenizer/web/web.go create mode 100644 analysis/tokenizer/web/web_test.go create mode 100644 analysis/tokenizer/whitespace/whitespace.go create mode 100644 analysis/tokenizer/whitespace/whitespace_test.go create mode 100644 analysis/tokenmap.go create mode 100644 analysis/tokenmap/custom.go create mode 100644 analysis/tokenmap_test.go create mode 100644 analysis/type.go create mode 100644 analysis/util.go create mode 100644 analysis/util_test.go create mode 100644 builder.go create mode 100644 builder_test.go create mode 100644 cmd/bleve/.gitignore create mode 100644 cmd/bleve/cmd/bulk.go create mode 100644 cmd/bleve/cmd/check.go create mode 100644 cmd/bleve/cmd/count.go create mode 100644 cmd/bleve/cmd/create.go create mode 100644 cmd/bleve/cmd/dictionary.go create mode 100644 cmd/bleve/cmd/dump.go create mode 100644 cmd/bleve/cmd/dumpDoc.go create mode 100644 cmd/bleve/cmd/dumpFields.go create mode 100644 cmd/bleve/cmd/fields.go create mode 100644 cmd/bleve/cmd/index.go create mode 100644 cmd/bleve/cmd/mapping.go create mode 100644 cmd/bleve/cmd/query.go create mode 100644 cmd/bleve/cmd/registry.go create mode 100644 cmd/bleve/cmd/root.go create mode 100644 cmd/bleve/cmd/scorch.go create mode 100644 cmd/bleve/cmd/scorch/ascii.go create mode 100644 cmd/bleve/cmd/scorch/deleted.go create mode 100644 cmd/bleve/cmd/scorch/info.go create mode 100644 cmd/bleve/cmd/scorch/internal.go create mode 100644 cmd/bleve/cmd/scorch/root.go create mode 100644 cmd/bleve/cmd/scorch/snapshot.go create mode 100644 cmd/bleve/gendocs.go create mode 100644 cmd/bleve/main.go create mode 100644 cmd/bleve/vendor/github.com/inconshreveable/mousetrap/LICENSE create mode 100644 cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_others.go create mode 100644 cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows.go create mode 100644 cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go create mode 100644 cmd/bleve/vendor/github.com/spf13/cobra/LICENSE.txt create mode 100644 cmd/bleve/vendor/github.com/spf13/cobra/bash_completions.go create mode 100644 cmd/bleve/vendor/github.com/spf13/cobra/cobra.go create mode 100644 cmd/bleve/vendor/github.com/spf13/cobra/command.go create mode 100644 cmd/bleve/vendor/github.com/spf13/cobra/command_notwin.go create mode 100644 cmd/bleve/vendor/github.com/spf13/cobra/command_win.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/LICENSE create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/bool.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/bool_slice.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/count.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/duration.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/flag.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/float32.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/float64.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/golangflag.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/int.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/int32.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/int64.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/int8.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/int_slice.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/ip.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/ip_slice.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/ipmask.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/ipnet.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/string.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/string_array.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/string_slice.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/uint.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/uint16.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/uint32.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/uint64.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/uint8.go create mode 100644 cmd/bleve/vendor/github.com/spf13/pflag/uint_slice.go create mode 100644 cmd/bleve/vendor/manifest create mode 100644 config.go create mode 100644 config/README.md create mode 100644 config/config.go create mode 100644 config_app.go create mode 100644 config_disk.go create mode 100644 data/test/sample-data.json create mode 100644 doc.go create mode 100644 docs/bleve.png create mode 100644 docs/geo.md create mode 100644 docs/scoring.md create mode 100644 docs/sort_facet.md create mode 100644 docs/sort_facet_supporting_docs/indexSizeVsNumDocs.png create mode 100644 docs/sort_facet_supporting_docs/queryTimevsNumDocs.png create mode 100644 docs/synonyms.md create mode 100644 docs/vectors.md create mode 100644 document/document.go create mode 100644 document/document_test.go create mode 100644 document/field.go create mode 100644 document/field_boolean.go create mode 100644 document/field_composite.go create mode 100644 document/field_datetime.go create mode 100644 document/field_geopoint.go create mode 100644 document/field_geopoint_test.go create mode 100644 document/field_geoshape.go create mode 100644 document/field_ip.go create mode 100644 document/field_ip_test.go create mode 100644 document/field_numeric.go create mode 100644 document/field_numeric_test.go create mode 100644 document/field_synonym.go create mode 100644 document/field_text.go create mode 100644 document/field_vector.go create mode 100644 document/field_vector_base64.go create mode 100644 document/field_vector_base64_test.go create mode 100644 error.go create mode 100644 examples_test.go create mode 100644 geo/README.md create mode 100644 geo/benchmark_geohash_test.go create mode 100644 geo/geo.go create mode 100644 geo/geo_dist.go create mode 100644 geo/geo_dist_test.go create mode 100644 geo/geo_s2plugin_impl.go create mode 100644 geo/geo_test.go create mode 100644 geo/geohash.go create mode 100644 geo/geohash_test.go create mode 100644 geo/parse.go create mode 100644 geo/parse_test.go create mode 100644 geo/sloppy.go create mode 100644 geo/versus_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 index.go create mode 100644 index/scorch/README.md create mode 100644 index/scorch/builder.go create mode 100644 index/scorch/builder_test.go create mode 100644 index/scorch/empty.go create mode 100644 index/scorch/event.go create mode 100644 index/scorch/event_test.go create mode 100644 index/scorch/field_dict_test.go create mode 100644 index/scorch/int.go create mode 100644 index/scorch/int_test.go create mode 100644 index/scorch/introducer.go create mode 100644 index/scorch/merge.go create mode 100644 index/scorch/merge_test.go create mode 100644 index/scorch/mergeplan/merge_plan.go create mode 100644 index/scorch/mergeplan/merge_plan_test.go create mode 100644 index/scorch/mergeplan/sort.go create mode 100644 index/scorch/optimize.go create mode 100644 index/scorch/optimize_knn.go create mode 100644 index/scorch/persister.go create mode 100644 index/scorch/reader_test.go create mode 100644 index/scorch/regexp.go create mode 100644 index/scorch/regexp_test.go create mode 100644 index/scorch/rollback.go create mode 100644 index/scorch/rollback_test.go create mode 100644 index/scorch/scorch.go create mode 100644 index/scorch/scorch_test.go create mode 100644 index/scorch/segment_plugin.go create mode 100644 index/scorch/snapshot_index.go create mode 100644 index/scorch/snapshot_index_dict.go create mode 100644 index/scorch/snapshot_index_doc.go create mode 100644 index/scorch/snapshot_index_str.go create mode 100644 index/scorch/snapshot_index_test.go create mode 100644 index/scorch/snapshot_index_tfr.go create mode 100644 index/scorch/snapshot_index_thes.go create mode 100644 index/scorch/snapshot_index_vr.go create mode 100644 index/scorch/snapshot_segment.go create mode 100644 index/scorch/snapshot_vector_index.go create mode 100644 index/scorch/stats.go create mode 100644 index/scorch/unadorned.go create mode 100644 index/upsidedown/analysis.go create mode 100644 index/upsidedown/analysis_test.go create mode 100755 index/upsidedown/benchmark_all.sh create mode 100644 index/upsidedown/benchmark_boltdb_test.go create mode 100644 index/upsidedown/benchmark_common_test.go create mode 100644 index/upsidedown/benchmark_gtreap_test.go create mode 100644 index/upsidedown/benchmark_null_test.go create mode 100644 index/upsidedown/dump.go create mode 100644 index/upsidedown/dump_test.go create mode 100644 index/upsidedown/field_cache.go create mode 100644 index/upsidedown/field_dict.go create mode 100644 index/upsidedown/field_dict_test.go create mode 100644 index/upsidedown/index_reader.go create mode 100644 index/upsidedown/reader.go create mode 100644 index/upsidedown/reader_test.go create mode 100644 index/upsidedown/row.go create mode 100644 index/upsidedown/row_merge.go create mode 100644 index/upsidedown/row_merge_test.go create mode 100644 index/upsidedown/row_test.go create mode 100644 index/upsidedown/stats.go create mode 100644 index/upsidedown/store/boltdb/iterator.go create mode 100644 index/upsidedown/store/boltdb/reader.go create mode 100644 index/upsidedown/store/boltdb/stats.go create mode 100644 index/upsidedown/store/boltdb/store.go create mode 100644 index/upsidedown/store/boltdb/store_test.go create mode 100644 index/upsidedown/store/boltdb/writer.go create mode 100644 index/upsidedown/store/goleveldb/batch.go create mode 100644 index/upsidedown/store/goleveldb/config.go create mode 100644 index/upsidedown/store/goleveldb/iterator.go create mode 100644 index/upsidedown/store/goleveldb/reader.go create mode 100644 index/upsidedown/store/goleveldb/store.go create mode 100644 index/upsidedown/store/goleveldb/store_test.go create mode 100644 index/upsidedown/store/goleveldb/writer.go create mode 100644 index/upsidedown/store/gtreap/iterator.go create mode 100644 index/upsidedown/store/gtreap/reader.go create mode 100644 index/upsidedown/store/gtreap/store.go create mode 100644 index/upsidedown/store/gtreap/store_test.go create mode 100644 index/upsidedown/store/gtreap/writer.go create mode 100644 index/upsidedown/store/metrics/batch.go create mode 100644 index/upsidedown/store/metrics/iterator.go create mode 100644 index/upsidedown/store/metrics/metrics_test.go create mode 100644 index/upsidedown/store/metrics/reader.go create mode 100644 index/upsidedown/store/metrics/stats.go create mode 100644 index/upsidedown/store/metrics/store.go create mode 100644 index/upsidedown/store/metrics/store_test.go create mode 100644 index/upsidedown/store/metrics/util.go create mode 100644 index/upsidedown/store/metrics/writer.go create mode 100644 index/upsidedown/store/moss/batch.go create mode 100644 index/upsidedown/store/moss/iterator.go create mode 100644 index/upsidedown/store/moss/lower.go create mode 100644 index/upsidedown/store/moss/lower_test.go create mode 100644 index/upsidedown/store/moss/reader.go create mode 100644 index/upsidedown/store/moss/stats.go create mode 100644 index/upsidedown/store/moss/store.go create mode 100644 index/upsidedown/store/moss/store_test.go create mode 100644 index/upsidedown/store/moss/writer.go create mode 100644 index/upsidedown/store/null/null.go create mode 100644 index/upsidedown/store/null/null_test.go create mode 100644 index/upsidedown/upsidedown.go create mode 100644 index/upsidedown/upsidedown.pb.go create mode 100644 index/upsidedown/upsidedown.proto create mode 100644 index/upsidedown/upsidedown_test.go create mode 100644 index_alias.go create mode 100644 index_alias_impl.go create mode 100644 index_alias_impl_test.go create mode 100644 index_impl.go create mode 100644 index_meta.go create mode 100644 index_meta_test.go create mode 100644 index_stats.go create mode 100644 index_test.go create mode 100644 mapping.go create mode 100644 mapping/analysis.go create mode 100644 mapping/document.go create mode 100644 mapping/examples_test.go create mode 100644 mapping/field.go create mode 100644 mapping/index.go create mode 100644 mapping/mapping.go create mode 100644 mapping/mapping_no_vectors.go create mode 100644 mapping/mapping_test.go create mode 100644 mapping/mapping_vectors.go create mode 100644 mapping/mapping_vectors_test.go create mode 100644 mapping/reflect.go create mode 100644 mapping/reflect_test.go create mode 100644 mapping/synonym.go create mode 100644 mapping_vector.go create mode 100644 numeric/bin.go create mode 100644 numeric/bin_test.go create mode 100644 numeric/float.go create mode 100644 numeric/float_test.go create mode 100644 numeric/prefix_coded.go create mode 100644 numeric/prefix_coded_test.go create mode 100644 pre_search.go create mode 100644 query.go create mode 100644 query_bench_test.go create mode 100644 registry/analyzer.go create mode 100644 registry/cache.go create mode 100644 registry/char_filter.go create mode 100644 registry/datetime_parser.go create mode 100644 registry/fragment_formatter.go create mode 100644 registry/fragmenter.go create mode 100644 registry/highlighter.go create mode 100644 registry/index_type.go create mode 100644 registry/registry.go create mode 100644 registry/store.go create mode 100644 registry/synonym_source.go create mode 100644 registry/token_filter.go create mode 100644 registry/token_maps.go create mode 100644 registry/tokenizer.go create mode 100755 scripts/build_children.sh create mode 100644 scripts/merge-coverprofile.go create mode 100644 scripts/old_build_script.txt create mode 100755 scripts/project-code-coverage.sh create mode 100644 search.go create mode 100644 search/collector.go create mode 100644 search/collector/bench_test.go create mode 100644 search/collector/eligible.go create mode 100644 search/collector/heap.go create mode 100644 search/collector/knn.go create mode 100644 search/collector/list.go create mode 100644 search/collector/search_test.go create mode 100644 search/collector/slice.go create mode 100644 search/collector/topn.go create mode 100644 search/collector/topn_test.go create mode 100644 search/explanation.go create mode 100644 search/facet/benchmark_data.txt create mode 100644 search/facet/facet_builder_datetime.go create mode 100644 search/facet/facet_builder_numeric.go create mode 100644 search/facet/facet_builder_numeric_test.go create mode 100644 search/facet/facet_builder_terms.go create mode 100644 search/facet/facet_builder_terms_test.go create mode 100644 search/facets_builder.go create mode 100644 search/facets_builder_test.go create mode 100644 search/highlight/format/ansi/ansi.go create mode 100644 search/highlight/format/html/html.go create mode 100644 search/highlight/format/html/html_test.go create mode 100644 search/highlight/format/plain/plain.go create mode 100644 search/highlight/format/plain/plain_test.go create mode 100644 search/highlight/fragmenter/simple/simple.go create mode 100644 search/highlight/fragmenter/simple/simple_test.go create mode 100644 search/highlight/highlighter.go create mode 100644 search/highlight/highlighter/ansi/ansi.go create mode 100644 search/highlight/highlighter/html/html.go create mode 100644 search/highlight/highlighter/simple/fragment_scorer_simple.go create mode 100644 search/highlight/highlighter/simple/fragment_scorer_simple_test.go create mode 100644 search/highlight/highlighter/simple/highlighter_simple.go create mode 100644 search/highlight/highlighter/simple/highlighter_simple_test.go create mode 100644 search/highlight/term_locations.go create mode 100644 search/highlight/term_locations_test.go create mode 100644 search/levenshtein.go create mode 100644 search/levenshtein_test.go create mode 100644 search/pool.go create mode 100644 search/pool_test.go create mode 100644 search/query/bool_field.go create mode 100644 search/query/boolean.go create mode 100644 search/query/boost.go create mode 100644 search/query/conjunction.go create mode 100644 search/query/date_range.go create mode 100644 search/query/date_range_string.go create mode 100644 search/query/date_range_test.go create mode 100644 search/query/disjunction.go create mode 100644 search/query/docid.go create mode 100644 search/query/fuzzy.go create mode 100644 search/query/geo_boundingbox.go create mode 100644 search/query/geo_boundingpolygon.go create mode 100644 search/query/geo_distance.go create mode 100644 search/query/geo_shape.go create mode 100644 search/query/ip_range.go create mode 100644 search/query/knn.go create mode 100644 search/query/match.go create mode 100644 search/query/match_all.go create mode 100644 search/query/match_none.go create mode 100644 search/query/match_phrase.go create mode 100644 search/query/match_phrase_test.go create mode 100644 search/query/multi_phrase.go create mode 100644 search/query/numeric_range.go create mode 100644 search/query/phrase.go create mode 100644 search/query/prefix.go create mode 100644 search/query/query.go create mode 100644 search/query/query_string.go create mode 100644 search/query/query_string.y create mode 100644 search/query/query_string.y.go create mode 100644 search/query/query_string_lex.go create mode 100644 search/query/query_string_lex_test.go create mode 100644 search/query/query_string_parser.go create mode 100644 search/query/query_string_parser_test.go create mode 100644 search/query/query_test.go create mode 100644 search/query/regexp.go create mode 100644 search/query/term.go create mode 100644 search/query/term_range.go create mode 100644 search/query/wildcard.go create mode 100644 search/scorer/scorer_conjunction.go create mode 100644 search/scorer/scorer_constant.go create mode 100644 search/scorer/scorer_constant_test.go create mode 100644 search/scorer/scorer_disjunction.go create mode 100644 search/scorer/scorer_knn.go create mode 100644 search/scorer/scorer_knn_test.go create mode 100644 search/scorer/scorer_term.go create mode 100644 search/scorer/scorer_term_test.go create mode 100644 search/scorer/sqrt_cache.go create mode 100644 search/search.go create mode 100644 search/search_test.go create mode 100644 search/searcher/base_test.go create mode 100644 search/searcher/geoshape_contains_test.go create mode 100644 search/searcher/geoshape_intersects_test.go create mode 100644 search/searcher/geoshape_within_test.go create mode 100644 search/searcher/optimize_knn.go create mode 100644 search/searcher/optimize_no_knn.go create mode 100644 search/searcher/ordered_searchers_list.go create mode 100644 search/searcher/search_boolean.go create mode 100644 search/searcher/search_boolean_test.go create mode 100644 search/searcher/search_conjunction.go create mode 100644 search/searcher/search_conjunction_test.go create mode 100644 search/searcher/search_disjunction.go create mode 100644 search/searcher/search_disjunction_heap.go create mode 100644 search/searcher/search_disjunction_slice.go create mode 100644 search/searcher/search_disjunction_test.go create mode 100644 search/searcher/search_docid.go create mode 100644 search/searcher/search_docid_test.go create mode 100644 search/searcher/search_filter.go create mode 100644 search/searcher/search_fuzzy.go create mode 100644 search/searcher/search_fuzzy_test.go create mode 100644 search/searcher/search_geoboundingbox.go create mode 100644 search/searcher/search_geoboundingbox_test.go create mode 100644 search/searcher/search_geopointdistance.go create mode 100644 search/searcher/search_geopointdistance_test.go create mode 100644 search/searcher/search_geopolygon.go create mode 100644 search/searcher/search_geopolygon_test.go create mode 100644 search/searcher/search_geoshape.go create mode 100644 search/searcher/search_geoshape_circle_test.go create mode 100644 search/searcher/search_geoshape_envelope_test.go create mode 100644 search/searcher/search_geoshape_geometrycollection_test.go create mode 100644 search/searcher/search_geoshape_linestring_test.go create mode 100644 search/searcher/search_geoshape_points_test.go create mode 100644 search/searcher/search_geoshape_polygon_test.go create mode 100644 search/searcher/search_ip_range.go create mode 100644 search/searcher/search_ip_range_test.go create mode 100644 search/searcher/search_knn.go create mode 100644 search/searcher/search_match_all.go create mode 100644 search/searcher/search_match_all_test.go create mode 100644 search/searcher/search_match_none.go create mode 100644 search/searcher/search_match_none_test.go create mode 100644 search/searcher/search_multi_term.go create mode 100644 search/searcher/search_numeric_range.go create mode 100644 search/searcher/search_numeric_range_test.go create mode 100644 search/searcher/search_phrase.go create mode 100644 search/searcher/search_phrase_test.go create mode 100644 search/searcher/search_regexp.go create mode 100644 search/searcher/search_regexp_test.go create mode 100644 search/searcher/search_term.go create mode 100644 search/searcher/search_term_prefix.go create mode 100644 search/searcher/search_term_range.go create mode 100644 search/searcher/search_term_range_test.go create mode 100644 search/searcher/search_term_test.go create mode 100644 search/sort.go create mode 100644 search/sort_test.go create mode 100644 search/util.go create mode 100644 search/util_test.go create mode 100644 search_knn.go create mode 100644 search_knn_test.go create mode 100644 search_no_knn.go create mode 100644 search_test.go create mode 100644 size/sizes.go create mode 100644 test/integration.go create mode 100644 test/integration_test.go create mode 100644 test/ip_field_test.go create mode 100644 test/knn/knn_dataset_queries.zip create mode 100644 test/tests/alias/datasets/shard0/a.json create mode 100644 test/tests/alias/datasets/shard0/c.json create mode 100644 test/tests/alias/datasets/shard1/b.json create mode 100644 test/tests/alias/datasets/shard1/d.json create mode 100644 test/tests/alias/mapping.json create mode 100644 test/tests/alias/searches.json create mode 100644 test/tests/basic/data/a.json create mode 100644 test/tests/basic/data/b.json create mode 100644 test/tests/basic/data/c.json create mode 100644 test/tests/basic/data/d.json create mode 100644 test/tests/basic/mapping.json create mode 100644 test/tests/basic/searches.json create mode 100644 test/tests/employee/data/emp10508560.json create mode 100644 test/tests/employee/mapping.json create mode 100644 test/tests/employee/searches.json create mode 100644 test/tests/facet/data/a.json create mode 100644 test/tests/facet/data/b.json create mode 100644 test/tests/facet/data/c.json create mode 100644 test/tests/facet/data/d.json create mode 100644 test/tests/facet/data/e.json create mode 100644 test/tests/facet/data/f.json create mode 100644 test/tests/facet/data/g.json create mode 100644 test/tests/facet/data/h.json create mode 100644 test/tests/facet/data/i.json create mode 100644 test/tests/facet/data/j.json create mode 100644 test/tests/facet/mapping.json create mode 100644 test/tests/facet/searches.json create mode 100644 test/tests/fosdem/data/3311@FOSDEM15@fosdem.org.json create mode 100644 test/tests/fosdem/data/3492@FOSDEM15@fosdem.org.json create mode 100644 test/tests/fosdem/data/3496@FOSDEM15@fosdem.org.json create mode 100644 test/tests/fosdem/data/3505@FOSDEM15@fosdem.org.json create mode 100644 test/tests/fosdem/data/3507@FOSDEM15@fosdem.org.json create mode 100644 test/tests/fosdem/mapping.json create mode 100644 test/tests/fosdem/searches.json create mode 100644 test/tests/geo/data/amoeba_brewery.json create mode 100644 test/tests/geo/data/brewpub_on_the_green.json create mode 100644 test/tests/geo/data/capital_city_brewing_company.json create mode 100644 test/tests/geo/data/communiti_brewery.json create mode 100644 test/tests/geo/data/firehouse_grill_brewery.json create mode 100644 test/tests/geo/data/hook_ladder_brewing_company.json create mode 100644 test/tests/geo/data/jack_s_brewing.json create mode 100644 test/tests/geo/data/social_brewery.json create mode 100644 test/tests/geo/data/sweet_water_tavern_and_brewery.json create mode 100644 test/tests/geo/mapping.json create mode 100644 test/tests/geo/searches.json create mode 100644 test/tests/geoshapes/data/circle_halairport.json create mode 100644 test/tests/geoshapes/data/envelope_brockwell_park.json create mode 100644 test/tests/geoshapes/data/geometrycollection_tvm.json create mode 100644 test/tests/geoshapes/data/linestring_putney_bridge.json create mode 100644 test/tests/geoshapes/data/multilinestring_old_airport_road.json create mode 100644 test/tests/geoshapes/data/multipoint_blr_stadiums.json create mode 100644 test/tests/geoshapes/data/multipolygon_london_parks.json create mode 100644 test/tests/geoshapes/data/point_museum_of_london.json create mode 100644 test/tests/geoshapes/data/polygon_cubbonpark.json create mode 100644 test/tests/geoshapes/mapping.json create mode 100644 test/tests/geoshapes/searches.json create mode 100644 test/tests/phrase/data/a.json create mode 100644 test/tests/phrase/data/b.json create mode 100644 test/tests/phrase/mapping.json create mode 100644 test/tests/phrase/searches.json create mode 100644 test/tests/sort/data/a.json create mode 100644 test/tests/sort/data/b.json create mode 100644 test/tests/sort/data/c.json create mode 100644 test/tests/sort/data/d.json create mode 100644 test/tests/sort/data/e.json create mode 100644 test/tests/sort/data/f.json create mode 100644 test/tests/sort/mapping.json create mode 100644 test/tests/sort/searches.json create mode 100644 test/versus_score_test.go create mode 100644 test/versus_test.go create mode 100644 util/extract.go create mode 100644 util/json.go diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e1e7e22 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +on: + push: + branches: + - master + pull_request: +name: Tests +jobs: + test: + strategy: + matrix: + go-version: [1.22.x, 1.23.x, 1.24.x] + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Install Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Test + run: | + go version + go test -race ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7512de7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +#* +*.sublime-* +*~ +.#* +.project +.settings +**/.idea/ +**/*.iml +.DS_Store +query_string.y.go.tmp +/analysis/token_filters/cld2/cld2-read-only +/analysis/token_filters/cld2/libcld2_full.a +/cmd/bleve/bleve +vendor/** +!vendor/manifest +/y.output +/search/query/y.output +*.test +tags +go.sum diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e6fa002 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +sudo: false + +language: go + +go: + - "1.21.x" + - "1.22.x" + - "1.23.x" + +script: + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + - go get github.com/kisielk/errcheck + - go get -u github.com/FiloSottile/gvt + - gvt restore + - go test -race -v $(go list ./... | grep -v vendor/) + - go vet $(go list ./... | grep -v vendor/) + - go test ./test -v -indexType scorch + - errcheck -ignorepkg fmt $(go list ./... | grep -v vendor/); + - scripts/project-code-coverage.sh + - scripts/build_children.sh + +notifications: + email: + - fts-team@couchbase.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5ebf3d6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing to Bleve + +We look forward to your contributions, but ask that you first review these guidelines. + +### Sign the CLA + +As Bleve is a Couchbase project we require contributors accept the [Couchbase Contributor License Agreement](http://review.couchbase.org/static/individual_agreement.html). To sign this agreement log into the Couchbase [code review tool](http://review.couchbase.org/). The Bleve project does not use this code review tool but it is still used to track acceptance of the contributor license agreements. + +### Submitting a Pull Request + +All types of contributions are welcome, but please keep the following in mind: + +- If you're planning a large change, you should really discuss it in a github issue or on the google group first. This helps avoid duplicate effort and spending time on something that may not be merged. +- Existing tests should continue to pass, new tests for the contribution are nice to have. +- All code should have gone through `go fmt` +- All code should pass `go vet` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef1a6dd --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# ![bleve](docs/bleve.png) bleve + +[![Tests](https://github.com/blevesearch/bleve/actions/workflows/tests.yml/badge.svg?branch=master&event=push)](https://github.com/blevesearch/bleve/actions/workflows/tests.yml?query=event%3Apush+branch%3Amaster) +[![Coverage Status](https://coveralls.io/repos/github/blevesearch/bleve/badge.svg?branch=master)](https://coveralls.io/github/blevesearch/bleve?branch=master) +[![Go Reference](https://pkg.go.dev/badge/github.com/blevesearch/bleve/v2.svg)](https://pkg.go.dev/github.com/blevesearch/bleve/v2) +[![Join the chat](https://badges.gitter.im/join_chat.svg)](https://app.gitter.im/#/room/#blevesearch_bleve:gitter.im) +[![codebeat](https://codebeat.co/badges/38a7cbc9-9cf5-41c0-a315-0746178230f4)](https://codebeat.co/projects/github-com-blevesearch-bleve) +[![Go Report Card](https://goreportcard.com/badge/github.com/blevesearch/bleve/v2)](https://goreportcard.com/report/github.com/blevesearch/bleve/v2) +[![Sourcegraph](https://sourcegraph.com/github.com/blevesearch/bleve/-/badge.svg)](https://sourcegraph.com/github.com/blevesearch/bleve?badge) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +A modern indexing + search library in GO + +## Features + +* Index any GO data structure or JSON +* Intelligent defaults backed up by powerful configuration ([scorch](https://github.com/blevesearch/bleve/blob/master/index/scorch/README.md)) +* Supported field types: + * `text`, `number`, `datetime`, `boolean`, `geopoint`, `geoshape`, `IP`, `vector` +* Supported query types: + * `term`, `phrase`, `match`, `match_phrase`, `prefix`, `regexp`, `wildcard`, `fuzzy` + * term range, numeric range, date range, boolean field + * compound queries: `conjuncts`, `disjuncts`, boolean (`must`/`should`/`must_not`) + * [query string syntax](http://www.blevesearch.com/docs/Query-String-Query/) + * [geo spatial search](https://github.com/blevesearch/bleve/blob/master/geo/README.md) + * approximate k-nearest neighbors via [vector search](https://github.com/blevesearch/bleve/blob/master/docs/vectors.md) + * [synonym search](https://github.com/blevesearch/bleve/blob/master/docs/synonyms.md) +* [tf-idf](https://github.com/blevesearch/bleve/blob/master/docs/scoring.md#tf-idf) / [bm25](https://github.com/blevesearch/bleve/blob/master/docs/scoring.md#bm25) scoring models +* Hybrid search: exact + semantic +* Query time boosting +* Search result match highlighting with document fragments +* Aggregations/faceting support: + * terms facet + * numeric range facet + * date range facet + +## Indexing + +```go +message := struct{ + Id string + From string + Body string +}{ + Id: "example", + From: "xyz@couchbase.com", + Body: "bleve indexing is easy", +} + +mapping := bleve.NewIndexMapping() +index, err := bleve.New("example.bleve", mapping) +if err != nil { + panic(err) +} +index.Index(message.Id, message) +``` + +## Querying + +```go +index, _ := bleve.Open("example.bleve") +query := bleve.NewQueryStringQuery("bleve") +searchRequest := bleve.NewSearchRequest(query) +searchResult, _ := index.Search(searchRequest) +``` + +## Command Line Interface + +To install the CLI for the latest release of bleve, run: + +```bash +$ go install github.com/blevesearch/bleve/v2/cmd/bleve@latest +``` + +``` +$ bleve --help +Bleve is a command-line tool to interact with a bleve index. + +Usage: + bleve [command] + +Available Commands: + bulk bulk loads from newline delimited JSON files + check checks the contents of the index + count counts the number documents in the index + create creates a new index + dictionary prints the term dictionary for the specified field in the index + dump dumps the contents of the index + fields lists the fields in this index + help Help about any command + index adds the files to the index + mapping prints the mapping used for this index + query queries the index + registry registry lists the bleve components compiled into this executable + scorch command-line tool to interact with a scorch index + +Flags: + -h, --help help for bleve + +Use "bleve [command] --help" for more information about a command. +``` + +## Text Analysis + +Bleve includes general-purpose analyzers (customizable) as well as pre-built text analyzers for the following languages: + +Arabic (ar), Bulgarian (bg), Catalan (ca), Chinese-Japanese-Korean (cjk), Kurdish (ckb), Danish (da), German (de), Greek (el), English (en), Spanish - Castilian (es), Basque (eu), Persian (fa), Finnish (fi), French (fr), Gaelic (ga), Spanish - Galician (gl), Hindi (hi), Croatian (hr), Hungarian (hu), Armenian (hy), Indonesian (id, in), Italian (it), Dutch (nl), Norwegian (no), Polish (pl), Portuguese (pt), Romanian (ro), Russian (ru), Swedish (sv), Turkish (tr) + +## Text Analysis Wizard + +[bleveanalysis.couchbase.com](https://bleveanalysis.couchbase.com) + +## Discussion/Issues + +Discuss usage/development of bleve and/or report issues here: +* [Github issues](https://github.com/blevesearch/bleve/issues) +* [Google group](https://groups.google.com/forum/#!forum/bleve) + +## License + +Apache License Version 2.0 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..51c6b6b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported Versions + +We support the latest release (for example, bleve v2.3.x). + +## Reporting a Vulnerability + +All security issues for this project should be reported by email to security@couchbase.com and fts-team@couchbase.com. +This mail will be delivered to the owners of this project. + +- To ensure your report is NOT marked as spam, please include the word "security/vulnerability" along with the project name (blevesearch/bleve) in the subject of the email. +- Please be as descriptive as possible while explaining the issue, and a testcase highlighting the issue is always welcome. + +Your email will be acknowledged at the soonest possible. diff --git a/analysis/analyzer/custom/custom.go b/analysis/analyzer/custom/custom.go new file mode 100644 index 0000000..5df940e --- /dev/null +++ b/analysis/analyzer/custom/custom.go @@ -0,0 +1,148 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package custom + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "custom" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + + var err error + var charFilters []analysis.CharFilter + charFiltersValue, ok := config["char_filters"] + if ok { + switch charFiltersValue := charFiltersValue.(type) { + case []string: + charFilters, err = getCharFilters(charFiltersValue, cache) + if err != nil { + return nil, err + } + case []interface{}: + charFiltersNames, err := convertInterfaceSliceToStringSlice(charFiltersValue, "char filter") + if err != nil { + return nil, err + } + charFilters, err = getCharFilters(charFiltersNames, cache) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported type for char_filters, must be slice") + } + } + + var tokenizerName string + tokenizerValue, ok := config["tokenizer"] + if ok { + tokenizerName, ok = tokenizerValue.(string) + if !ok { + return nil, fmt.Errorf("must specify tokenizer as string") + } + } else { + return nil, fmt.Errorf("must specify tokenizer") + } + + tokenizer, err := cache.TokenizerNamed(tokenizerName) + if err != nil { + return nil, err + } + + var tokenFilters []analysis.TokenFilter + tokenFiltersValue, ok := config["token_filters"] + if ok { + switch tokenFiltersValue := tokenFiltersValue.(type) { + case []string: + tokenFilters, err = getTokenFilters(tokenFiltersValue, cache) + if err != nil { + return nil, err + } + case []interface{}: + tokenFiltersNames, err := convertInterfaceSliceToStringSlice(tokenFiltersValue, "token filter") + if err != nil { + return nil, err + } + tokenFilters, err = getTokenFilters(tokenFiltersNames, cache) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported type for token_filters, must be slice") + } + } + + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + } + if charFilters != nil { + rv.CharFilters = charFilters + } + if tokenFilters != nil { + rv.TokenFilters = tokenFilters + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(Name, AnalyzerConstructor) + if err != nil { + panic(err) + } +} + +func getCharFilters(charFilterNames []string, cache *registry.Cache) ([]analysis.CharFilter, error) { + charFilters := make([]analysis.CharFilter, len(charFilterNames)) + for i, charFilterName := range charFilterNames { + charFilter, err := cache.CharFilterNamed(charFilterName) + if err != nil { + return nil, err + } + charFilters[i] = charFilter + } + + return charFilters, nil +} + +func getTokenFilters(tokenFilterNames []string, cache *registry.Cache) ([]analysis.TokenFilter, error) { + tokenFilters := make([]analysis.TokenFilter, len(tokenFilterNames)) + for i, tokenFilterName := range tokenFilterNames { + tokenFilter, err := cache.TokenFilterNamed(tokenFilterName) + if err != nil { + return nil, err + } + tokenFilters[i] = tokenFilter + } + + return tokenFilters, nil +} + +func convertInterfaceSliceToStringSlice(interfaceSlice []interface{}, objType string) ([]string, error) { + stringSlice := make([]string, len(interfaceSlice)) + for i, interfaceObj := range interfaceSlice { + stringObj, ok := interfaceObj.(string) + if ok { + stringSlice[i] = stringObj + } else { + return nil, fmt.Errorf(objType + " name must be a string") + } + } + + return stringSlice, nil +} diff --git a/analysis/analyzer/keyword/keyword.go b/analysis/analyzer/keyword/keyword.go new file mode 100644 index 0000000..6eb052e --- /dev/null +++ b/analysis/analyzer/keyword/keyword.go @@ -0,0 +1,41 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package keyword + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/single" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "keyword" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + keywordTokenizer, err := cache.TokenizerNamed(single.Name) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: keywordTokenizer, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(Name, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/analyzer/simple/simple.go b/analysis/analyzer/simple/simple.go new file mode 100644 index 0000000..2a9834b --- /dev/null +++ b/analysis/analyzer/simple/simple.go @@ -0,0 +1,49 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package simple + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/letter" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "simple" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(letter.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(Name, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/analyzer/standard/standard.go b/analysis/analyzer/standard/standard.go new file mode 100644 index 0000000..fa752be --- /dev/null +++ b/analysis/analyzer/standard/standard.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/lang/en" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "standard" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopEnFilter, err := cache.TokenFilterNamed(en.StopName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopEnFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(Name, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/analyzer/web/web.go b/analysis/analyzer/web/web.go new file mode 100644 index 0000000..778586e --- /dev/null +++ b/analysis/analyzer/web/web.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package web + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/lang/en" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/web" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "web" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(web.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopEnFilter, err := cache.TokenFilterNamed(en.StopName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopEnFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(Name, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/benchmark_test.go b/analysis/benchmark_test.go new file mode 100644 index 0000000..c5b6647 --- /dev/null +++ b/analysis/benchmark_test.go @@ -0,0 +1,117 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis_test + +import ( + index "github.com/blevesearch/bleve_index_api" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + "github.com/blevesearch/bleve/v2/registry" +) + +func BenchmarkAnalysis(b *testing.B) { + for i := 0; i < b.N; i++ { + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(standard.Name) + if err != nil { + b.Fatal(err) + } + + ts := analyzer.Analyze(bleveWikiArticle) + freqs := analysis.TokenFrequency(ts, nil, index.IncludeTermVectors) + if len(freqs) != 511 { + b.Errorf("expected %d freqs, got %d", 511, len(freqs)) + } + } +} + +var bleveWikiArticle = []byte(`Boiling liquid expanding vapor explosion +From Wikipedia, the free encyclopedia +See also: Boiler explosion and Steam explosion + +Flames subsequent to a flammable liquid BLEVE from a tanker. BLEVEs do not necessarily involve fire. + +This article's tone or style may not reflect the encyclopedic tone used on Wikipedia. See Wikipedia's guide to writing better articles for suggestions. (July 2013) +A boiling liquid expanding vapor explosion (BLEVE, /ˈblɛviː/ blev-ee) is an explosion caused by the rupture of a vessel containing a pressurized liquid above its boiling point.[1] +Contents [hide] +1 Mechanism +1.1 Water example +1.2 BLEVEs without chemical reactions +2 Fires +3 Incidents +4 Safety measures +5 See also +6 References +7 External links +Mechanism[edit] + +This section needs additional citations for verification. Please help improve this article by adding citations to reliable sources. Unsourced material may be challenged and removed. (July 2013) +There are three characteristics of liquids which are relevant to the discussion of a BLEVE: +If a liquid in a sealed container is boiled, the pressure inside the container increases. As the liquid changes to a gas it expands - this expansion in a vented container would cause the gas and liquid to take up more space. In a sealed container the gas and liquid are not able to take up more space and so the pressure rises. Pressurized vessels containing liquids can reach an equilibrium where the liquid stops boiling and the pressure stops rising. This occurs when no more heat is being added to the system (either because it has reached ambient temperature or has had a heat source removed). +The boiling temperature of a liquid is dependent on pressure - high pressures will yield high boiling temperatures, and low pressures will yield low boiling temperatures. A common simple experiment is to place a cup of water in a vacuum chamber, and then reduce the pressure in the chamber until the water boils. By reducing the pressure the water will boil even at room temperature. This works both ways - if the pressure is increased beyond normal atmospheric pressures, the boiling of hot water could be suppressed far beyond normal temperatures. The cooling system of a modern internal combustion engine is a real-world example. +When a liquid boils it turns into a gas. The resulting gas takes up far more space than the liquid did. +Typically, a BLEVE starts with a container of liquid which is held above its normal, atmospheric-pressure boiling temperature. Many substances normally stored as liquids, such as CO2, propane, and other similar industrial gases have boiling temperatures, at atmospheric pressure, far below room temperature. In the case of water, a BLEVE could occur if a pressurized chamber of water is heated far beyond the standard 100 °C (212 °F). That container, because the boiling water pressurizes it, is capable of holding liquid water at very high temperatures. +If the pressurized vessel, containing liquid at high temperature (which may be room temperature, depending on the substance) ruptures, the pressure which prevents the liquid from boiling is lost. If the rupture is catastrophic, where the vessel is immediately incapable of holding any pressure at all, then there suddenly exists a large mass of liquid which is at very high temperature and very low pressure. This causes the entire volume of liquid to instantaneously boil, which in turn causes an extremely rapid expansion. Depending on temperatures, pressures and the substance involved, that expansion may be so rapid that it can be classified as an explosion, fully capable of inflicting severe damage on its surroundings. +Water example[edit] +Imagine, for example, a tank of pressurized liquid water held at 204.4 °C (400 °F). This tank would normally be pressurized to 1.7 MPa (250 psi) above atmospheric ("gauge") pressure. If the tank containing the water were to rupture, there would for a slight moment exist a volume of liquid water which would be +at atmospheric pressure, and +204.4 °C (400 °F). +At atmospheric pressure the boiling point of water is 100 °C (212 °F) - liquid water at atmospheric pressure cannot exist at temperatures higher than 100 °C (212 °F). At that moment, the water would boil and turn to vapour explosively, and the 204.4 °C (400 °F) liquid water turned to gas would take up a lot more volume than it did as liquid, causing a vapour explosion. Such explosions can happen when the superheated water of a steam engine escapes through a crack in a boiler, causing a boiler explosion. +BLEVEs without chemical reactions[edit] +It is important to note that a BLEVE need not be a chemical explosion—nor does there need to be a fire—however if a flammable substance is subject to a BLEVE it may also be subject to intense heating, either from an external source of heat which may have caused the vessel to rupture in the first place or from an internal source of localized heating such as skin friction. This heating can cause a flammable substance to ignite, adding a secondary explosion caused by the primary BLEVE. While blast effects of any BLEVE can be devastating, a flammable substance such as propane can add significantly to the danger. +Bleve explosion.svg +While the term BLEVE is most often used to describe the results of a container of flammable liquid rupturing due to fire, a BLEVE can occur even with a non-flammable substance such as water,[2] liquid nitrogen,[3] liquid helium or other refrigerants or cryogens, and therefore is not usually considered a type of chemical explosion. +Fires[edit] +BLEVEs can be caused by an external fire near the storage vessel causing heating of the contents and pressure build-up. While tanks are often designed to withstand great pressure, constant heating can cause the metal to weaken and eventually fail. If the tank is being heated in an area where there is no liquid, it may rupture faster without the liquid to absorb the heat. Gas containers are usually equipped with relief valves that vent off excess pressure, but the tank can still fail if the pressure is not released quickly enough.[1] Relief valves are sized to release pressure fast enough to prevent the pressure from increasing beyond the strength of the vessel, but not so fast as to be the cause of an explosion. An appropriately sized relief valve will allow the liquid inside to boil slowly, maintaining a constant pressure in the vessel until all the liquid has boiled and the vessel empties. +If the substance involved is flammable, it is likely that the resulting cloud of the substance will ignite after the BLEVE has occurred, forming a fireball and possibly a fuel-air explosion, also termed a vapor cloud explosion (VCE). If the materials are toxic, a large area will be contaminated.[4] +Incidents[edit] +The term "BLEVE" was coined by three researchers at Factory Mutual, in the analysis of an accident there in 1957 involving a chemical reactor vessel.[5] +In August 1959 the Kansas City Fire Department suffered its largest ever loss of life in the line of duty, when a 25,000 gallon (95,000 litre) gas tank exploded during a fire on Southwest Boulevard killing five firefighters. This was the first time BLEVE was used to describe a burning fuel tank.[citation needed] +Later incidents included the Cheapside Street Whisky Bond Fire in Glasgow, Scotland in 1960; Feyzin, France in 1966; Crescent City, Illinois in 1970; Kingman, Arizona in 1973; a liquid nitrogen tank rupture[6] at Air Products and Chemicals and Mobay Chemical Company at New Martinsville, West Virginia on January 31, 1978 [1];Texas City, Texas in 1978; Murdock, Illinois in 1983; San Juan Ixhuatepec, Mexico City in 1984; and Toronto, Ontario in 2008. +Safety measures[edit] +[icon] This section requires expansion. (July 2013) +Some fire mitigation measures are listed under liquefied petroleum gas. +See also[edit] +Boiler explosion +Expansion ratio +Explosive boiling or phase explosion +Rapid phase transition +Viareggio train derailment +2008 Toronto explosions +Gas carriers +Los Alfaques Disaster +Lac-Mégantic derailment +References[edit] +^ Jump up to: a b Kletz, Trevor (March 1990). Critical Aspects of Safety and Loss Prevention. London: Butterworth–Heinemann. pp. 43–45. ISBN 0-408-04429-2. +Jump up ^ "Temperature Pressure Relief Valves on Water Heaters: test, inspect, replace, repair guide". Inspect-ny.com. Retrieved 2011-07-12. +Jump up ^ Liquid nitrogen BLEVE demo +Jump up ^ "Chemical Process Safety" (PDF). Retrieved 2011-07-12. +Jump up ^ David F. Peterson, BLEVE: Facts, Risk Factors, and Fallacies, Fire Engineering magazine (2002). +Jump up ^ "STATE EX REL. VAPOR CORP. v. NARICK". Supreme Court of Appeals of West Virginia. 1984-07-12. Retrieved 2014-03-16. +External links[edit] + Look up boiling liquid expanding vapor explosion in Wiktionary, the free dictionary. + Wikimedia Commons has media related to BLEVE. +BLEVE Demo on YouTube — video of a controlled BLEVE demo +huge explosions on YouTube — video of propane and isobutane BLEVEs from a train derailment at Murdock, Illinois (3 September 1983) +Propane BLEVE on YouTube — video of BLEVE from the Toronto propane depot fire +Moscow Ring Road Accident on YouTube - Dozens of LPG tank BLEVEs after a road accident in Moscow +Kingman, AZ BLEVE — An account of the 5 July 1973 explosion in Kingman, with photographs +Propane Tank Explosions — Description of circumstances required to cause a propane tank BLEVE. +Analysis of BLEVE Events at DOE Sites - Details physics and mathematics of BLEVEs. +HID - SAFETY REPORT ASSESSMENT GUIDE: Whisky Maturation Warehouses - The liquor is aged in wooden barrels that can suffer BLEVE. +Categories: ExplosivesFirefightingFireTypes of fireGas technologiesIndustrial fires and explosions`) diff --git a/analysis/char/asciifolding/asciifolding.go b/analysis/char/asciifolding/asciifolding.go new file mode 100644 index 0000000..9e21c61 --- /dev/null +++ b/analysis/char/asciifolding/asciifolding.go @@ -0,0 +1,3572 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// converted to Go from Lucene's AsciiFoldingFilter +// https://lucene.apache.org/core/4_0_0/analyzers-common/org/apache/lucene/analysis/miscellaneous/ASCIIFoldingFilter.html + +package asciifolding + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "asciifolding" + +type AsciiFoldingFilter struct{} + +func New() *AsciiFoldingFilter { + return &AsciiFoldingFilter{} +} + +func (s *AsciiFoldingFilter) Filter(input []byte) []byte { + if len(input) == 0 { + return input + } + + in := []rune(string(input)) + length := len(in) + + // Worst-case length required if all runes fold to 4 runes + out := make([]rune, length, length*4) + + out = foldToASCII(in, 0, out, 0, length) + return []byte(string(out)) +} + +func AsciiFoldingFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.CharFilter, error) { + return New(), nil +} + +func init() { + err := registry.RegisterCharFilter(Name, AsciiFoldingFilterConstructor) + if err != nil { + panic(err) + } +} + +// Converts characters above ASCII to their ASCII equivalents. +// For example, accents are removed from accented characters. +func foldToASCII(input []rune, inputPos int, output []rune, outputPos int, length int) []rune { + end := inputPos + length + for pos := inputPos; pos < end; pos++ { + c := input[pos] + + // Quick test: if it's not in range then just keep current character + if c < '\u0080' { + output[outputPos] = c + outputPos++ + } else { + switch c { + case '\u00C0': // À [LATIN CAPITAL LETTER A WITH GRAVE] + fallthrough + case '\u00C1': // Á [LATIN CAPITAL LETTER A WITH ACUTE] + fallthrough + case '\u00C2': //  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] + fallthrough + case '\u00C3': // à [LATIN CAPITAL LETTER A WITH TILDE] + fallthrough + case '\u00C4': // Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] + fallthrough + case '\u00C5': // Å [LATIN CAPITAL LETTER A WITH RING ABOVE] + fallthrough + case '\u0100': // Ā [LATIN CAPITAL LETTER A WITH MACRON] + fallthrough + case '\u0102': // Ă [LATIN CAPITAL LETTER A WITH BREVE] + fallthrough + case '\u0104': // Ą [LATIN CAPITAL LETTER A WITH OGONEK] + fallthrough + case '\u018F': // Ə http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] + fallthrough + case '\u01CD': // Ǎ [LATIN CAPITAL LETTER A WITH CARON] + fallthrough + case '\u01DE': // Ǟ [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] + fallthrough + case '\u01E0': // Ǡ [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] + fallthrough + case '\u01FA': // Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] + fallthrough + case '\u0200': // Ȁ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] + fallthrough + case '\u0202': // Ȃ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] + fallthrough + case '\u0226': // Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] + fallthrough + case '\u023A': // Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] + fallthrough + case '\u1D00': // ᴀ [LATIN LETTER SMALL CAPITAL A] + fallthrough + case '\u1E00': // Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] + fallthrough + case '\u1EA0': // Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] + fallthrough + case '\u1EA2': // Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] + fallthrough + case '\u1EA4': // Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] + fallthrough + case '\u1EA6': // Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] + fallthrough + case '\u1EA8': // Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + fallthrough + case '\u1EAA': // Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] + fallthrough + case '\u1EAC': // Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + fallthrough + case '\u1EAE': // Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] + fallthrough + case '\u1EB0': // Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] + fallthrough + case '\u1EB2': // Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] + fallthrough + case '\u1EB4': // Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] + fallthrough + case '\u24B6': // Ⓐ [CIRCLED LATIN CAPITAL LETTER A] + fallthrough + case '\uFF21': // A [FULLWIDTH LATIN CAPITAL LETTER A] + fallthrough + case '\u1EB6': // Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] + output[outputPos] = 'A' + outputPos++ + + case '\u00E0': // à [LATIN SMALL LETTER A WITH GRAVE] + fallthrough + case '\u00E1': // á [LATIN SMALL LETTER A WITH ACUTE] + fallthrough + case '\u00E2': // â [LATIN SMALL LETTER A WITH CIRCUMFLEX] + fallthrough + case '\u00E3': // ã [LATIN SMALL LETTER A WITH TILDE] + fallthrough + case '\u00E4': // ä [LATIN SMALL LETTER A WITH DIAERESIS] + fallthrough + case '\u00E5': // å [LATIN SMALL LETTER A WITH RING ABOVE] + fallthrough + case '\u0101': // ā [LATIN SMALL LETTER A WITH MACRON] + fallthrough + case '\u0103': // ă [LATIN SMALL LETTER A WITH BREVE] + fallthrough + case '\u0105': // ą [LATIN SMALL LETTER A WITH OGONEK] + fallthrough + case '\u01CE': // ǎ [LATIN SMALL LETTER A WITH CARON] + fallthrough + case '\u01DF': // ǟ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] + fallthrough + case '\u01E1': // ǡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] + fallthrough + case '\u01FB': // ǻ [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] + fallthrough + case '\u0201': // ȁ [LATIN SMALL LETTER A WITH DOUBLE GRAVE] + fallthrough + case '\u0203': // ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] + fallthrough + case '\u0227': // ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] + fallthrough + case '\u0250': // ɐ [LATIN SMALL LETTER TURNED A] + fallthrough + case '\u0259': // ə [LATIN SMALL LETTER SCHWA] + fallthrough + case '\u025A': // ɚ [LATIN SMALL LETTER SCHWA WITH HOOK] + fallthrough + case '\u1D8F': // ᶏ [LATIN SMALL LETTER A WITH RETROFLEX HOOK] + fallthrough + case '\u1D95': // ᶕ [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] + fallthrough + case '\u1E01': // ạ [LATIN SMALL LETTER A WITH RING BELOW] + fallthrough + case '\u1E9A': // ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] + fallthrough + case '\u1EA1': // ạ [LATIN SMALL LETTER A WITH DOT BELOW] + fallthrough + case '\u1EA3': // ả [LATIN SMALL LETTER A WITH HOOK ABOVE] + fallthrough + case '\u1EA5': // ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] + fallthrough + case '\u1EA7': // ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] + fallthrough + case '\u1EA9': // ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] + fallthrough + case '\u1EAB': // ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] + fallthrough + case '\u1EAD': // ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] + fallthrough + case '\u1EAF': // ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] + fallthrough + case '\u1EB1': // ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] + fallthrough + case '\u1EB3': // ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] + fallthrough + case '\u1EB5': // ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] + fallthrough + case '\u1EB7': // ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] + fallthrough + case '\u2090': // ₐ [LATIN SUBSCRIPT SMALL LETTER A] + fallthrough + case '\u2094': // ₔ [LATIN SUBSCRIPT SMALL LETTER SCHWA] + fallthrough + case '\u24D0': // ⓐ [CIRCLED LATIN SMALL LETTER A] + fallthrough + case '\u2C65': // ⱥ [LATIN SMALL LETTER A WITH STROKE] + fallthrough + case '\u2C6F': // Ɐ [LATIN CAPITAL LETTER TURNED A] + fallthrough + case '\uFF41': // a [FULLWIDTH LATIN SMALL LETTER A] + output[outputPos] = 'a' + outputPos++ + + case '\uA732': // Ꜳ [LATIN CAPITAL LETTER AA] + output = output[:(len(output) + 1)] + output[outputPos] = 'A' + outputPos++ + output[outputPos] = 'A' + outputPos++ + + case '\u00C6': // Æ [LATIN CAPITAL LETTER AE] + fallthrough + case '\u01E2': // Ǣ [LATIN CAPITAL LETTER AE WITH MACRON] + fallthrough + case '\u01FC': // Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] + fallthrough + case '\u1D01': // ᴁ [LATIN LETTER SMALL CAPITAL AE] + output = output[:(len(output) + 1)] + output[outputPos] = 'A' + outputPos++ + output[outputPos] = 'E' + outputPos++ + + case '\uA734': // Ꜵ [LATIN CAPITAL LETTER AO] + output = output[:(len(output) + 1)] + output[outputPos] = 'A' + outputPos++ + output[outputPos] = 'O' + outputPos++ + + case '\uA736': // Ꜷ [LATIN CAPITAL LETTER AU] + output = output[:(len(output) + 1)] + output[outputPos] = 'A' + outputPos++ + output[outputPos] = 'U' + outputPos++ + + case '\uA738': // Ꜹ [LATIN CAPITAL LETTER AV] + fallthrough + case '\uA73A': // Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] + output = output[:(len(output) + 1)] + output[outputPos] = 'A' + outputPos++ + output[outputPos] = 'V' + outputPos++ + + case '\uA73C': // Ꜽ [LATIN CAPITAL LETTER AY] + output = output[:(len(output) + 1)] + output[outputPos] = 'A' + outputPos++ + output[outputPos] = 'Y' + outputPos++ + + case '\u249C': // ⒜ [PARENTHESIZED LATIN SMALL LETTER A] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'a' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\uA733': // ꜳ [LATIN SMALL LETTER AA] + output = output[:(len(output) + 1)] + output[outputPos] = 'a' + outputPos++ + output[outputPos] = 'a' + outputPos++ + + case '\u00E6': // æ [LATIN SMALL LETTER AE] + fallthrough + case '\u01E3': // ǣ [LATIN SMALL LETTER AE WITH MACRON] + fallthrough + case '\u01FD': // ǽ [LATIN SMALL LETTER AE WITH ACUTE] + fallthrough + case '\u1D02': // ᴂ [LATIN SMALL LETTER TURNED AE] + output = output[:(len(output) + 1)] + output[outputPos] = 'a' + outputPos++ + output[outputPos] = 'e' + outputPos++ + + case '\uA735': // ꜵ [LATIN SMALL LETTER AO] + output = output[:(len(output) + 1)] + output[outputPos] = 'a' + outputPos++ + output[outputPos] = 'o' + outputPos++ + + case '\uA737': // ꜷ [LATIN SMALL LETTER AU] + output = output[:(len(output) + 1)] + output[outputPos] = 'a' + outputPos++ + output[outputPos] = 'u' + outputPos++ + + case '\uA739': // ꜹ [LATIN SMALL LETTER AV] + fallthrough + case '\uA73B': // ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] + output = output[:(len(output) + 1)] + output[outputPos] = 'a' + outputPos++ + output[outputPos] = 'v' + outputPos++ + + case '\uA73D': // ꜽ [LATIN SMALL LETTER AY] + output = output[:(len(output) + 1)] + output[outputPos] = 'a' + outputPos++ + output[outputPos] = 'y' + outputPos++ + + case '\u0181': // Ɓ [LATIN CAPITAL LETTER B WITH HOOK] + fallthrough + case '\u0182': // Ƃ [LATIN CAPITAL LETTER B WITH TOPBAR] + fallthrough + case '\u0243': // Ƀ [LATIN CAPITAL LETTER B WITH STROKE] + fallthrough + case '\u0299': // ʙ [LATIN LETTER SMALL CAPITAL B] + fallthrough + case '\u1D03': // ᴃ [LATIN LETTER SMALL CAPITAL BARRED B] + fallthrough + case '\u1E02': // Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] + fallthrough + case '\u1E04': // Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] + fallthrough + case '\u1E06': // Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] + fallthrough + case '\u24B7': // Ⓑ [CIRCLED LATIN CAPITAL LETTER B] + fallthrough + case '\uFF22': // B [FULLWIDTH LATIN CAPITAL LETTER B] + output[outputPos] = 'B' + outputPos++ + + case '\u0180': // ƀ [LATIN SMALL LETTER B WITH STROKE] + fallthrough + case '\u0183': // ƃ [LATIN SMALL LETTER B WITH TOPBAR] + fallthrough + case '\u0253': // ɓ [LATIN SMALL LETTER B WITH HOOK] + fallthrough + case '\u1D6C': // ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] + fallthrough + case '\u1D80': // ᶀ [LATIN SMALL LETTER B WITH PALATAL HOOK] + fallthrough + case '\u1E03': // ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] + fallthrough + case '\u1E05': // ḅ [LATIN SMALL LETTER B WITH DOT BELOW] + fallthrough + case '\u1E07': // ḇ [LATIN SMALL LETTER B WITH LINE BELOW] + fallthrough + case '\u24D1': // ⓑ [CIRCLED LATIN SMALL LETTER B] + fallthrough + case '\uFF42': // b [FULLWIDTH LATIN SMALL LETTER B] + output[outputPos] = 'b' + outputPos++ + + case '\u249D': // ⒝ [PARENTHESIZED LATIN SMALL LETTER B] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'b' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00C7': // Ç [LATIN CAPITAL LETTER C WITH CEDILLA] + fallthrough + case '\u0106': // Ć [LATIN CAPITAL LETTER C WITH ACUTE] + fallthrough + case '\u0108': // Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] + fallthrough + case '\u010A': // Ċ [LATIN CAPITAL LETTER C WITH DOT ABOVE] + fallthrough + case '\u010C': // Č [LATIN CAPITAL LETTER C WITH CARON] + fallthrough + case '\u0187': // Ƈ [LATIN CAPITAL LETTER C WITH HOOK] + fallthrough + case '\u023B': // Ȼ [LATIN CAPITAL LETTER C WITH STROKE] + fallthrough + case '\u0297': // ʗ [LATIN LETTER STRETCHED C] + fallthrough + case '\u1D04': // ᴄ [LATIN LETTER SMALL CAPITAL C] + fallthrough + case '\u1E08': // Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] + fallthrough + case '\u24B8': // Ⓒ [CIRCLED LATIN CAPITAL LETTER C] + fallthrough + case '\uFF23': // C [FULLWIDTH LATIN CAPITAL LETTER C] + output[outputPos] = 'C' + outputPos++ + + case '\u00E7': // ç [LATIN SMALL LETTER C WITH CEDILLA] + fallthrough + case '\u0107': // ć [LATIN SMALL LETTER C WITH ACUTE] + fallthrough + case '\u0109': // ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] + fallthrough + case '\u010B': // ċ [LATIN SMALL LETTER C WITH DOT ABOVE] + fallthrough + case '\u010D': // č [LATIN SMALL LETTER C WITH CARON] + fallthrough + case '\u0188': // ƈ [LATIN SMALL LETTER C WITH HOOK] + fallthrough + case '\u023C': // ȼ [LATIN SMALL LETTER C WITH STROKE] + fallthrough + case '\u0255': // ɕ [LATIN SMALL LETTER C WITH CURL] + fallthrough + case '\u1E09': // ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] + fallthrough + case '\u2184': // ↄ [LATIN SMALL LETTER REVERSED C] + fallthrough + case '\u24D2': // ⓒ [CIRCLED LATIN SMALL LETTER C] + fallthrough + case '\uA73E': // Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] + fallthrough + case '\uA73F': // ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] + fallthrough + case '\uFF43': // c [FULLWIDTH LATIN SMALL LETTER C] + output[outputPos] = 'c' + outputPos++ + + case '\u249E': // ⒞ [PARENTHESIZED LATIN SMALL LETTER C] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'c' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00D0': // Ð [LATIN CAPITAL LETTER ETH] + fallthrough + case '\u010E': // Ď [LATIN CAPITAL LETTER D WITH CARON] + fallthrough + case '\u0110': // Đ [LATIN CAPITAL LETTER D WITH STROKE] + fallthrough + case '\u0189': // Ɖ [LATIN CAPITAL LETTER AFRICAN D] + fallthrough + case '\u018A': // Ɗ [LATIN CAPITAL LETTER D WITH HOOK] + fallthrough + case '\u018B': // Ƌ [LATIN CAPITAL LETTER D WITH TOPBAR] + fallthrough + case '\u1D05': // ᴅ [LATIN LETTER SMALL CAPITAL D] + fallthrough + case '\u1D06': // ᴆ [LATIN LETTER SMALL CAPITAL ETH] + fallthrough + case '\u1E0A': // Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] + fallthrough + case '\u1E0C': // Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] + fallthrough + case '\u1E0E': // Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] + fallthrough + case '\u1E10': // Ḑ [LATIN CAPITAL LETTER D WITH CEDILLA] + fallthrough + case '\u1E12': // Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] + fallthrough + case '\u24B9': // Ⓓ [CIRCLED LATIN CAPITAL LETTER D] + fallthrough + case '\uA779': // Ꝺ [LATIN CAPITAL LETTER INSULAR D] + fallthrough + case '\uFF24': // D [FULLWIDTH LATIN CAPITAL LETTER D] + output[outputPos] = 'D' + outputPos++ + + case '\u00F0': // ð [LATIN SMALL LETTER ETH] + fallthrough + case '\u010F': // ď [LATIN SMALL LETTER D WITH CARON] + fallthrough + case '\u0111': // đ [LATIN SMALL LETTER D WITH STROKE] + fallthrough + case '\u018C': // ƌ [LATIN SMALL LETTER D WITH TOPBAR] + fallthrough + case '\u0221': // ȡ [LATIN SMALL LETTER D WITH CURL] + fallthrough + case '\u0256': // ɖ [LATIN SMALL LETTER D WITH TAIL] + fallthrough + case '\u0257': // ɗ [LATIN SMALL LETTER D WITH HOOK] + fallthrough + case '\u1D6D': // ᵭ [LATIN SMALL LETTER D WITH MIDDLE TILDE] + fallthrough + case '\u1D81': // ᶁ [LATIN SMALL LETTER D WITH PALATAL HOOK] + fallthrough + case '\u1D91': // ᶑ [LATIN SMALL LETTER D WITH HOOK AND TAIL] + fallthrough + case '\u1E0B': // ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] + fallthrough + case '\u1E0D': // ḍ [LATIN SMALL LETTER D WITH DOT BELOW] + fallthrough + case '\u1E0F': // ḏ [LATIN SMALL LETTER D WITH LINE BELOW] + fallthrough + case '\u1E11': // ḑ [LATIN SMALL LETTER D WITH CEDILLA] + fallthrough + case '\u1E13': // ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] + fallthrough + case '\u24D3': // ⓓ [CIRCLED LATIN SMALL LETTER D] + fallthrough + case '\uA77A': // ꝺ [LATIN SMALL LETTER INSULAR D] + fallthrough + case '\uFF44': // d [FULLWIDTH LATIN SMALL LETTER D] + output[outputPos] = 'd' + outputPos++ + + case '\u01C4': // DŽ [LATIN CAPITAL LETTER DZ WITH CARON] + fallthrough + case '\u01F1': // DZ [LATIN CAPITAL LETTER DZ] + output = output[:(len(output) + 1)] + output[outputPos] = 'D' + outputPos++ + output[outputPos] = 'Z' + outputPos++ + + case '\u01C5': // Dž [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] + fallthrough + case '\u01F2': // Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] + output = output[:(len(output) + 1)] + output[outputPos] = 'D' + outputPos++ + output[outputPos] = 'z' + outputPos++ + + case '\u249F': // ⒟ [PARENTHESIZED LATIN SMALL LETTER D] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'd' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0238': // ȸ [LATIN SMALL LETTER DB DIGRAPH] + output = output[:(len(output) + 1)] + output[outputPos] = 'd' + outputPos++ + output[outputPos] = 'b' + outputPos++ + + case '\u01C6': // dž [LATIN SMALL LETTER DZ WITH CARON] + fallthrough + case '\u01F3': // dz [LATIN SMALL LETTER DZ] + fallthrough + case '\u02A3': // ʣ [LATIN SMALL LETTER DZ DIGRAPH] + fallthrough + case '\u02A5': // ʥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] + output = output[:(len(output) + 1)] + output[outputPos] = 'd' + outputPos++ + output[outputPos] = 'z' + outputPos++ + + case '\u00C8': // È [LATIN CAPITAL LETTER E WITH GRAVE] + fallthrough + case '\u00C9': // É [LATIN CAPITAL LETTER E WITH ACUTE] + fallthrough + case '\u00CA': // Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] + fallthrough + case '\u00CB': // Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] + fallthrough + case '\u0112': // Ē [LATIN CAPITAL LETTER E WITH MACRON] + fallthrough + case '\u0114': // Ĕ [LATIN CAPITAL LETTER E WITH BREVE] + fallthrough + case '\u0116': // Ė [LATIN CAPITAL LETTER E WITH DOT ABOVE] + fallthrough + case '\u0118': // Ę [LATIN CAPITAL LETTER E WITH OGONEK] + fallthrough + case '\u011A': // Ě [LATIN CAPITAL LETTER E WITH CARON] + fallthrough + case '\u018E': // Ǝ [LATIN CAPITAL LETTER REVERSED E] + fallthrough + case '\u0190': // Ɛ [LATIN CAPITAL LETTER OPEN E] + fallthrough + case '\u0204': // Ȅ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] + fallthrough + case '\u0206': // Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] + fallthrough + case '\u0228': // Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] + fallthrough + case '\u0246': // Ɇ [LATIN CAPITAL LETTER E WITH STROKE] + fallthrough + case '\u1D07': // ᴇ [LATIN LETTER SMALL CAPITAL E] + fallthrough + case '\u1E14': // Ḕ [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] + fallthrough + case '\u1E16': // Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] + fallthrough + case '\u1E18': // Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] + fallthrough + case '\u1E1A': // Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] + fallthrough + case '\u1E1C': // Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] + fallthrough + case '\u1EB8': // Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] + fallthrough + case '\u1EBA': // Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] + fallthrough + case '\u1EBC': // Ẽ [LATIN CAPITAL LETTER E WITH TILDE] + fallthrough + case '\u1EBE': // Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] + fallthrough + case '\u1EC0': // Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] + fallthrough + case '\u1EC2': // Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + fallthrough + case '\u1EC4': // Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] + fallthrough + case '\u1EC6': // Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + fallthrough + case '\u24BA': // Ⓔ [CIRCLED LATIN CAPITAL LETTER E] + fallthrough + case '\u2C7B': // ⱻ [LATIN LETTER SMALL CAPITAL TURNED E] + fallthrough + case '\uFF25': // E [FULLWIDTH LATIN CAPITAL LETTER E] + output[outputPos] = 'E' + outputPos++ + + case '\u00E8': // è [LATIN SMALL LETTER E WITH GRAVE] + fallthrough + case '\u00E9': // é [LATIN SMALL LETTER E WITH ACUTE] + fallthrough + case '\u00EA': // ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] + fallthrough + case '\u00EB': // ë [LATIN SMALL LETTER E WITH DIAERESIS] + fallthrough + case '\u0113': // ē [LATIN SMALL LETTER E WITH MACRON] + fallthrough + case '\u0115': // ĕ [LATIN SMALL LETTER E WITH BREVE] + fallthrough + case '\u0117': // ė [LATIN SMALL LETTER E WITH DOT ABOVE] + fallthrough + case '\u0119': // ę [LATIN SMALL LETTER E WITH OGONEK] + fallthrough + case '\u011B': // ě [LATIN SMALL LETTER E WITH CARON] + fallthrough + case '\u01DD': // ǝ [LATIN SMALL LETTER TURNED E] + fallthrough + case '\u0205': // ȅ [LATIN SMALL LETTER E WITH DOUBLE GRAVE] + fallthrough + case '\u0207': // ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] + fallthrough + case '\u0229': // ȩ [LATIN SMALL LETTER E WITH CEDILLA] + fallthrough + case '\u0247': // ɇ [LATIN SMALL LETTER E WITH STROKE] + fallthrough + case '\u0258': // ɘ [LATIN SMALL LETTER REVERSED E] + fallthrough + case '\u025B': // ɛ [LATIN SMALL LETTER OPEN E] + fallthrough + case '\u025C': // ɜ [LATIN SMALL LETTER REVERSED OPEN E] + fallthrough + case '\u025D': // ɝ [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] + fallthrough + case '\u025E': // ɞ [LATIN SMALL LETTER CLOSED REVERSED OPEN E] + fallthrough + case '\u029A': // ʚ [LATIN SMALL LETTER CLOSED OPEN E] + fallthrough + case '\u1D08': // ᴈ [LATIN SMALL LETTER TURNED OPEN E] + fallthrough + case '\u1D92': // ᶒ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] + fallthrough + case '\u1D93': // ᶓ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] + fallthrough + case '\u1D94': // ᶔ [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] + fallthrough + case '\u1E15': // ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] + fallthrough + case '\u1E17': // ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] + fallthrough + case '\u1E19': // ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] + fallthrough + case '\u1E1B': // ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] + fallthrough + case '\u1E1D': // ḝ [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] + fallthrough + case '\u1EB9': // ẹ [LATIN SMALL LETTER E WITH DOT BELOW] + fallthrough + case '\u1EBB': // ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] + fallthrough + case '\u1EBD': // ẽ [LATIN SMALL LETTER E WITH TILDE] + fallthrough + case '\u1EBF': // ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] + fallthrough + case '\u1EC1': // ề [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] + fallthrough + case '\u1EC3': // ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] + fallthrough + case '\u1EC5': // ễ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] + fallthrough + case '\u1EC7': // ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] + fallthrough + case '\u2091': // ₑ [LATIN SUBSCRIPT SMALL LETTER E] + fallthrough + case '\u24D4': // ⓔ [CIRCLED LATIN SMALL LETTER E] + fallthrough + case '\u2C78': // ⱸ [LATIN SMALL LETTER E WITH NOTCH] + fallthrough + case '\uFF45': // e [FULLWIDTH LATIN SMALL LETTER E] + output[outputPos] = 'e' + outputPos++ + + case '\u24A0': // ⒠ [PARENTHESIZED LATIN SMALL LETTER E] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'e' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0191': // Ƒ [LATIN CAPITAL LETTER F WITH HOOK] + fallthrough + case '\u1E1E': // Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] + fallthrough + case '\u24BB': // Ⓕ [CIRCLED LATIN CAPITAL LETTER F] + fallthrough + case '\uA730': // ꜰ [LATIN LETTER SMALL CAPITAL F] + fallthrough + case '\uA77B': // Ꝼ [LATIN CAPITAL LETTER INSULAR F] + fallthrough + case '\uA7FB': // ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] + fallthrough + case '\uFF26': // F [FULLWIDTH LATIN CAPITAL LETTER F] + output[outputPos] = 'F' + outputPos++ + + case '\u0192': // ƒ [LATIN SMALL LETTER F WITH HOOK] + fallthrough + case '\u1D6E': // ᵮ [LATIN SMALL LETTER F WITH MIDDLE TILDE] + fallthrough + case '\u1D82': // ᶂ [LATIN SMALL LETTER F WITH PALATAL HOOK] + fallthrough + case '\u1E1F': // ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] + fallthrough + case '\u1E9B': // ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] + fallthrough + case '\u24D5': // ⓕ [CIRCLED LATIN SMALL LETTER F] + fallthrough + case '\uA77C': // ꝼ [LATIN SMALL LETTER INSULAR F] + fallthrough + case '\uFF46': // f [FULLWIDTH LATIN SMALL LETTER F] + output[outputPos] = 'f' + outputPos++ + + case '\u24A1': // ⒡ [PARENTHESIZED LATIN SMALL LETTER F] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'f' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\uFB00': // ff [LATIN SMALL LIGATURE FF] + output = output[:(len(output) + 1)] + output[outputPos] = 'f' + outputPos++ + output[outputPos] = 'f' + outputPos++ + + case '\uFB03': // ffi [LATIN SMALL LIGATURE FFI] + output = output[:(len(output) + 2)] + output[outputPos] = 'f' + outputPos++ + output[outputPos] = 'f' + outputPos++ + output[outputPos] = 'i' + outputPos++ + + case '\uFB04': // ffl [LATIN SMALL LIGATURE FFL] + output = output[:(len(output) + 2)] + output[outputPos] = 'f' + outputPos++ + output[outputPos] = 'f' + outputPos++ + output[outputPos] = 'l' + outputPos++ + + case '\uFB01': // fi [LATIN SMALL LIGATURE FI] + output = output[:(len(output) + 1)] + output[outputPos] = 'f' + outputPos++ + output[outputPos] = 'i' + outputPos++ + + case '\uFB02': // fl [LATIN SMALL LIGATURE FL] + output = output[:(len(output) + 1)] + output[outputPos] = 'f' + outputPos++ + output[outputPos] = 'l' + outputPos++ + + case '\u011C': // Ĝ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] + fallthrough + case '\u011E': // Ğ [LATIN CAPITAL LETTER G WITH BREVE] + fallthrough + case '\u0120': // Ġ [LATIN CAPITAL LETTER G WITH DOT ABOVE] + fallthrough + case '\u0122': // Ģ [LATIN CAPITAL LETTER G WITH CEDILLA] + fallthrough + case '\u0193': // Ɠ [LATIN CAPITAL LETTER G WITH HOOK] + fallthrough + case '\u01E4': // Ǥ [LATIN CAPITAL LETTER G WITH STROKE] + fallthrough + case '\u01E5': // ǥ [LATIN SMALL LETTER G WITH STROKE] + fallthrough + case '\u01E6': // Ǧ [LATIN CAPITAL LETTER G WITH CARON] + fallthrough + case '\u01E7': // ǧ [LATIN SMALL LETTER G WITH CARON] + fallthrough + case '\u01F4': // Ǵ [LATIN CAPITAL LETTER G WITH ACUTE] + fallthrough + case '\u0262': // ɢ [LATIN LETTER SMALL CAPITAL G] + fallthrough + case '\u029B': // ʛ [LATIN LETTER SMALL CAPITAL G WITH HOOK] + fallthrough + case '\u1E20': // Ḡ [LATIN CAPITAL LETTER G WITH MACRON] + fallthrough + case '\u24BC': // Ⓖ [CIRCLED LATIN CAPITAL LETTER G] + fallthrough + case '\uA77D': // Ᵹ [LATIN CAPITAL LETTER INSULAR G] + fallthrough + case '\uA77E': // Ꝿ [LATIN CAPITAL LETTER TURNED INSULAR G] + fallthrough + case '\uFF27': // G [FULLWIDTH LATIN CAPITAL LETTER G] + output[outputPos] = 'G' + outputPos++ + + case '\u011D': // ĝ [LATIN SMALL LETTER G WITH CIRCUMFLEX] + fallthrough + case '\u011F': // ğ [LATIN SMALL LETTER G WITH BREVE] + fallthrough + case '\u0121': // ġ [LATIN SMALL LETTER G WITH DOT ABOVE] + fallthrough + case '\u0123': // ģ [LATIN SMALL LETTER G WITH CEDILLA] + fallthrough + case '\u01F5': // ǵ [LATIN SMALL LETTER G WITH ACUTE] + fallthrough + case '\u0260': // ɠ [LATIN SMALL LETTER G WITH HOOK] + fallthrough + case '\u0261': // ɡ [LATIN SMALL LETTER SCRIPT G] + fallthrough + case '\u1D77': // ᵷ [LATIN SMALL LETTER TURNED G] + fallthrough + case '\u1D79': // ᵹ [LATIN SMALL LETTER INSULAR G] + fallthrough + case '\u1D83': // ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] + fallthrough + case '\u1E21': // ḡ [LATIN SMALL LETTER G WITH MACRON] + fallthrough + case '\u24D6': // ⓖ [CIRCLED LATIN SMALL LETTER G] + fallthrough + case '\uA77F': // ꝿ [LATIN SMALL LETTER TURNED INSULAR G] + fallthrough + case '\uFF47': // g [FULLWIDTH LATIN SMALL LETTER G] + output[outputPos] = 'g' + outputPos++ + + case '\u24A2': // ⒢ [PARENTHESIZED LATIN SMALL LETTER G] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'g' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0124': // Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] + fallthrough + case '\u0126': // Ħ [LATIN CAPITAL LETTER H WITH STROKE] + fallthrough + case '\u021E': // Ȟ [LATIN CAPITAL LETTER H WITH CARON] + fallthrough + case '\u029C': // ʜ [LATIN LETTER SMALL CAPITAL H] + fallthrough + case '\u1E22': // Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] + fallthrough + case '\u1E24': // Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] + fallthrough + case '\u1E26': // Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] + fallthrough + case '\u1E28': // Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] + fallthrough + case '\u1E2A': // Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] + fallthrough + case '\u24BD': // Ⓗ [CIRCLED LATIN CAPITAL LETTER H] + fallthrough + case '\u2C67': // Ⱨ [LATIN CAPITAL LETTER H WITH DESCENDER] + fallthrough + case '\u2C75': // Ⱶ [LATIN CAPITAL LETTER HALF H] + fallthrough + case '\uFF28': // H [FULLWIDTH LATIN CAPITAL LETTER H] + output[outputPos] = 'H' + outputPos++ + + case '\u0125': // ĥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] + fallthrough + case '\u0127': // ħ [LATIN SMALL LETTER H WITH STROKE] + fallthrough + case '\u021F': // ȟ [LATIN SMALL LETTER H WITH CARON] + fallthrough + case '\u0265': // ɥ [LATIN SMALL LETTER TURNED H] + fallthrough + case '\u0266': // ɦ [LATIN SMALL LETTER H WITH HOOK] + fallthrough + case '\u02AE': // ʮ [LATIN SMALL LETTER TURNED H WITH FISHHOOK] + fallthrough + case '\u02AF': // ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] + fallthrough + case '\u1E23': // ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] + fallthrough + case '\u1E25': // ḥ [LATIN SMALL LETTER H WITH DOT BELOW] + fallthrough + case '\u1E27': // ḧ [LATIN SMALL LETTER H WITH DIAERESIS] + fallthrough + case '\u1E29': // ḩ [LATIN SMALL LETTER H WITH CEDILLA] + fallthrough + case '\u1E2B': // ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] + fallthrough + case '\u1E96': // ẖ [LATIN SMALL LETTER H WITH LINE BELOW] + fallthrough + case '\u24D7': // ⓗ [CIRCLED LATIN SMALL LETTER H] + fallthrough + case '\u2C68': // ⱨ [LATIN SMALL LETTER H WITH DESCENDER] + fallthrough + case '\u2C76': // ⱶ [LATIN SMALL LETTER HALF H] + fallthrough + case '\uFF48': // h [FULLWIDTH LATIN SMALL LETTER H] + output[outputPos] = 'h' + outputPos++ + + case '\u01F6': // Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] + output = output[:(len(output) + 1)] + output[outputPos] = 'H' + outputPos++ + output[outputPos] = 'V' + outputPos++ + + case '\u24A3': // ⒣ [PARENTHESIZED LATIN SMALL LETTER H] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'h' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0195': // ƕ [LATIN SMALL LETTER HV] + output = output[:(len(output) + 1)] + output[outputPos] = 'h' + outputPos++ + output[outputPos] = 'v' + outputPos++ + + case '\u00CC': // Ì [LATIN CAPITAL LETTER I WITH GRAVE] + fallthrough + case '\u00CD': // Í [LATIN CAPITAL LETTER I WITH ACUTE] + fallthrough + case '\u00CE': // Î [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] + fallthrough + case '\u00CF': // Ï [LATIN CAPITAL LETTER I WITH DIAERESIS] + fallthrough + case '\u0128': // Ĩ [LATIN CAPITAL LETTER I WITH TILDE] + fallthrough + case '\u012A': // Ī [LATIN CAPITAL LETTER I WITH MACRON] + fallthrough + case '\u012C': // Ĭ [LATIN CAPITAL LETTER I WITH BREVE] + fallthrough + case '\u012E': // Į [LATIN CAPITAL LETTER I WITH OGONEK] + fallthrough + case '\u0130': // İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] + fallthrough + case '\u0196': // Ɩ [LATIN CAPITAL LETTER IOTA] + fallthrough + case '\u0197': // Ɨ [LATIN CAPITAL LETTER I WITH STROKE] + fallthrough + case '\u01CF': // Ǐ [LATIN CAPITAL LETTER I WITH CARON] + fallthrough + case '\u0208': // Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] + fallthrough + case '\u020A': // Ȋ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] + fallthrough + case '\u026A': // ɪ [LATIN LETTER SMALL CAPITAL I] + fallthrough + case '\u1D7B': // ᵻ [LATIN SMALL CAPITAL LETTER I WITH STROKE] + fallthrough + case '\u1E2C': // Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] + fallthrough + case '\u1E2E': // Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] + fallthrough + case '\u1EC8': // Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] + fallthrough + case '\u1ECA': // Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] + fallthrough + case '\u24BE': // Ⓘ [CIRCLED LATIN CAPITAL LETTER I] + fallthrough + case '\uA7FE': // ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] + fallthrough + case '\uFF29': // I [FULLWIDTH LATIN CAPITAL LETTER I] + output[outputPos] = 'I' + outputPos++ + + case '\u00EC': // ì [LATIN SMALL LETTER I WITH GRAVE] + fallthrough + case '\u00ED': // í [LATIN SMALL LETTER I WITH ACUTE] + fallthrough + case '\u00EE': // î [LATIN SMALL LETTER I WITH CIRCUMFLEX] + fallthrough + case '\u00EF': // ï [LATIN SMALL LETTER I WITH DIAERESIS] + fallthrough + case '\u0129': // ĩ [LATIN SMALL LETTER I WITH TILDE] + fallthrough + case '\u012B': // ī [LATIN SMALL LETTER I WITH MACRON] + fallthrough + case '\u012D': // ĭ [LATIN SMALL LETTER I WITH BREVE] + fallthrough + case '\u012F': // į [LATIN SMALL LETTER I WITH OGONEK] + fallthrough + case '\u0131': // ı [LATIN SMALL LETTER DOTLESS I] + fallthrough + case '\u01D0': // ǐ [LATIN SMALL LETTER I WITH CARON] + fallthrough + case '\u0209': // ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] + fallthrough + case '\u020B': // ȋ [LATIN SMALL LETTER I WITH INVERTED BREVE] + fallthrough + case '\u0268': // ɨ [LATIN SMALL LETTER I WITH STROKE] + fallthrough + case '\u1D09': // ᴉ [LATIN SMALL LETTER TURNED I] + fallthrough + case '\u1D62': // ᵢ [LATIN SUBSCRIPT SMALL LETTER I] + fallthrough + case '\u1D7C': // ᵼ [LATIN SMALL LETTER IOTA WITH STROKE] + fallthrough + case '\u1D96': // ᶖ [LATIN SMALL LETTER I WITH RETROFLEX HOOK] + fallthrough + case '\u1E2D': // ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] + fallthrough + case '\u1E2F': // ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] + fallthrough + case '\u1EC9': // ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] + fallthrough + case '\u1ECB': // ị [LATIN SMALL LETTER I WITH DOT BELOW] + fallthrough + case '\u2071': // ⁱ [SUPERSCRIPT LATIN SMALL LETTER I] + fallthrough + case '\u24D8': // ⓘ [CIRCLED LATIN SMALL LETTER I] + fallthrough + case '\uFF49': // i [FULLWIDTH LATIN SMALL LETTER I] + output[outputPos] = 'i' + outputPos++ + + case '\u0132': // IJ [LATIN CAPITAL LIGATURE IJ] + output = output[:(len(output) + 1)] + output[outputPos] = 'I' + outputPos++ + output[outputPos] = 'J' + outputPos++ + + case '\u24A4': // ⒤ [PARENTHESIZED LATIN SMALL LETTER I] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'i' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0133': // ij [LATIN SMALL LIGATURE IJ] + output = output[:(len(output) + 1)] + output[outputPos] = 'i' + outputPos++ + output[outputPos] = 'j' + outputPos++ + + case '\u0134': // Ĵ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] + fallthrough + case '\u0248': // Ɉ [LATIN CAPITAL LETTER J WITH STROKE] + fallthrough + case '\u1D0A': // ᴊ [LATIN LETTER SMALL CAPITAL J] + fallthrough + case '\u24BF': // Ⓙ [CIRCLED LATIN CAPITAL LETTER J] + fallthrough + case '\uFF2A': // J [FULLWIDTH LATIN CAPITAL LETTER J] + output[outputPos] = 'J' + outputPos++ + + case '\u0135': // ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] + fallthrough + case '\u01F0': // ǰ [LATIN SMALL LETTER J WITH CARON] + fallthrough + case '\u0237': // ȷ [LATIN SMALL LETTER DOTLESS J] + fallthrough + case '\u0249': // ɉ [LATIN SMALL LETTER J WITH STROKE] + fallthrough + case '\u025F': // ɟ [LATIN SMALL LETTER DOTLESS J WITH STROKE] + fallthrough + case '\u0284': // ʄ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] + fallthrough + case '\u029D': // ʝ [LATIN SMALL LETTER J WITH CROSSED-TAIL] + fallthrough + case '\u24D9': // ⓙ [CIRCLED LATIN SMALL LETTER J] + fallthrough + case '\u2C7C': // ⱼ [LATIN SUBSCRIPT SMALL LETTER J] + fallthrough + case '\uFF4A': // j [FULLWIDTH LATIN SMALL LETTER J] + output[outputPos] = 'j' + outputPos++ + + case '\u24A5': // ⒥ [PARENTHESIZED LATIN SMALL LETTER J] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'j' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0136': // Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] + fallthrough + case '\u0198': // Ƙ [LATIN CAPITAL LETTER K WITH HOOK] + fallthrough + case '\u01E8': // Ǩ [LATIN CAPITAL LETTER K WITH CARON] + fallthrough + case '\u1D0B': // ᴋ [LATIN LETTER SMALL CAPITAL K] + fallthrough + case '\u1E30': // Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] + fallthrough + case '\u1E32': // Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] + fallthrough + case '\u1E34': // Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] + fallthrough + case '\u24C0': // Ⓚ [CIRCLED LATIN CAPITAL LETTER K] + fallthrough + case '\u2C69': // Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] + fallthrough + case '\uA740': // Ꝁ [LATIN CAPITAL LETTER K WITH STROKE] + fallthrough + case '\uA742': // Ꝃ [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] + fallthrough + case '\uA744': // Ꝅ [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] + fallthrough + case '\uFF2B': // K [FULLWIDTH LATIN CAPITAL LETTER K] + output[outputPos] = 'K' + outputPos++ + + case '\u0137': // ķ [LATIN SMALL LETTER K WITH CEDILLA] + fallthrough + case '\u0199': // ƙ [LATIN SMALL LETTER K WITH HOOK] + fallthrough + case '\u01E9': // ǩ [LATIN SMALL LETTER K WITH CARON] + fallthrough + case '\u029E': // ʞ [LATIN SMALL LETTER TURNED K] + fallthrough + case '\u1D84': // ᶄ [LATIN SMALL LETTER K WITH PALATAL HOOK] + fallthrough + case '\u1E31': // ḱ [LATIN SMALL LETTER K WITH ACUTE] + fallthrough + case '\u1E33': // ḳ [LATIN SMALL LETTER K WITH DOT BELOW] + fallthrough + case '\u1E35': // ḵ [LATIN SMALL LETTER K WITH LINE BELOW] + fallthrough + case '\u24DA': // ⓚ [CIRCLED LATIN SMALL LETTER K] + fallthrough + case '\u2C6A': // ⱪ [LATIN SMALL LETTER K WITH DESCENDER] + fallthrough + case '\uA741': // ꝁ [LATIN SMALL LETTER K WITH STROKE] + fallthrough + case '\uA743': // ꝃ [LATIN SMALL LETTER K WITH DIAGONAL STROKE] + fallthrough + case '\uA745': // ꝅ [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] + fallthrough + case '\uFF4B': // k [FULLWIDTH LATIN SMALL LETTER K] + output[outputPos] = 'k' + outputPos++ + + case '\u24A6': // ⒦ [PARENTHESIZED LATIN SMALL LETTER K] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'k' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0139': // Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] + fallthrough + case '\u013B': // Ļ [LATIN CAPITAL LETTER L WITH CEDILLA] + fallthrough + case '\u013D': // Ľ [LATIN CAPITAL LETTER L WITH CARON] + fallthrough + case '\u013F': // Ŀ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] + fallthrough + case '\u0141': // Ł [LATIN CAPITAL LETTER L WITH STROKE] + fallthrough + case '\u023D': // Ƚ [LATIN CAPITAL LETTER L WITH BAR] + fallthrough + case '\u029F': // ʟ [LATIN LETTER SMALL CAPITAL L] + fallthrough + case '\u1D0C': // ᴌ [LATIN LETTER SMALL CAPITAL L WITH STROKE] + fallthrough + case '\u1E36': // Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] + fallthrough + case '\u1E38': // Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] + fallthrough + case '\u1E3A': // Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] + fallthrough + case '\u1E3C': // Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] + fallthrough + case '\u24C1': // Ⓛ [CIRCLED LATIN CAPITAL LETTER L] + fallthrough + case '\u2C60': // Ⱡ [LATIN CAPITAL LETTER L WITH DOUBLE BAR] + fallthrough + case '\u2C62': // Ɫ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] + fallthrough + case '\uA746': // Ꝇ [LATIN CAPITAL LETTER BROKEN L] + fallthrough + case '\uA748': // Ꝉ [LATIN CAPITAL LETTER L WITH HIGH STROKE] + fallthrough + case '\uA780': // Ꞁ [LATIN CAPITAL LETTER TURNED L] + fallthrough + case '\uFF2C': // L [FULLWIDTH LATIN CAPITAL LETTER L] + output[outputPos] = 'L' + outputPos++ + + case '\u013A': // ĺ [LATIN SMALL LETTER L WITH ACUTE] + fallthrough + case '\u013C': // ļ [LATIN SMALL LETTER L WITH CEDILLA] + fallthrough + case '\u013E': // ľ [LATIN SMALL LETTER L WITH CARON] + fallthrough + case '\u0140': // ŀ [LATIN SMALL LETTER L WITH MIDDLE DOT] + fallthrough + case '\u0142': // ł [LATIN SMALL LETTER L WITH STROKE] + fallthrough + case '\u019A': // ƚ [LATIN SMALL LETTER L WITH BAR] + fallthrough + case '\u0234': // ȴ [LATIN SMALL LETTER L WITH CURL] + fallthrough + case '\u026B': // ɫ [LATIN SMALL LETTER L WITH MIDDLE TILDE] + fallthrough + case '\u026C': // ɬ [LATIN SMALL LETTER L WITH BELT] + fallthrough + case '\u026D': // ɭ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] + fallthrough + case '\u1D85': // ᶅ [LATIN SMALL LETTER L WITH PALATAL HOOK] + fallthrough + case '\u1E37': // ḷ [LATIN SMALL LETTER L WITH DOT BELOW] + fallthrough + case '\u1E39': // ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] + fallthrough + case '\u1E3B': // ḻ [LATIN SMALL LETTER L WITH LINE BELOW] + fallthrough + case '\u1E3D': // ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] + fallthrough + case '\u24DB': // ⓛ [CIRCLED LATIN SMALL LETTER L] + fallthrough + case '\u2C61': // ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] + fallthrough + case '\uA747': // ꝇ [LATIN SMALL LETTER BROKEN L] + fallthrough + case '\uA749': // ꝉ [LATIN SMALL LETTER L WITH HIGH STROKE] + fallthrough + case '\uA781': // ꞁ [LATIN SMALL LETTER TURNED L] + fallthrough + case '\uFF4C': // l [FULLWIDTH LATIN SMALL LETTER L] + output[outputPos] = 'l' + outputPos++ + + case '\u01C7': // LJ [LATIN CAPITAL LETTER LJ] + output = output[:(len(output) + 1)] + output[outputPos] = 'L' + outputPos++ + output[outputPos] = 'J' + outputPos++ + + case '\u1EFA': // Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] + output = output[:(len(output) + 1)] + output[outputPos] = 'L' + outputPos++ + output[outputPos] = 'L' + outputPos++ + + case '\u01C8': // Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] + output = output[:(len(output) + 1)] + output[outputPos] = 'L' + outputPos++ + output[outputPos] = 'j' + outputPos++ + + case '\u24A7': // ⒧ [PARENTHESIZED LATIN SMALL LETTER L] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'l' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u01C9': // lj [LATIN SMALL LETTER LJ] + output = output[:(len(output) + 1)] + output[outputPos] = 'l' + outputPos++ + output[outputPos] = 'j' + outputPos++ + + case '\u1EFB': // ỻ [LATIN SMALL LETTER MIDDLE-WELSH LL] + output = output[:(len(output) + 1)] + output[outputPos] = 'l' + outputPos++ + output[outputPos] = 'l' + outputPos++ + + case '\u02AA': // ʪ [LATIN SMALL LETTER LS DIGRAPH] + output = output[:(len(output) + 1)] + output[outputPos] = 'l' + outputPos++ + output[outputPos] = 's' + outputPos++ + + case '\u02AB': // ʫ [LATIN SMALL LETTER LZ DIGRAPH] + output = output[:(len(output) + 1)] + output[outputPos] = 'l' + outputPos++ + output[outputPos] = 'z' + outputPos++ + + case '\u019C': // Ɯ [LATIN CAPITAL LETTER TURNED M] + fallthrough + case '\u1D0D': // ᴍ [LATIN LETTER SMALL CAPITAL M] + fallthrough + case '\u1E3E': // Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] + fallthrough + case '\u1E40': // Ṁ [LATIN CAPITAL LETTER M WITH DOT ABOVE] + fallthrough + case '\u1E42': // Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] + fallthrough + case '\u24C2': // Ⓜ [CIRCLED LATIN CAPITAL LETTER M] + fallthrough + case '\u2C6E': // Ɱ [LATIN CAPITAL LETTER M WITH HOOK] + fallthrough + case '\uA7FD': // ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] + fallthrough + case '\uA7FF': // ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] + fallthrough + case '\uFF2D': // M [FULLWIDTH LATIN CAPITAL LETTER M] + output[outputPos] = 'M' + outputPos++ + + case '\u026F': // ɯ [LATIN SMALL LETTER TURNED M] + fallthrough + case '\u0270': // ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] + fallthrough + case '\u0271': // ɱ [LATIN SMALL LETTER M WITH HOOK] + fallthrough + case '\u1D6F': // ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] + fallthrough + case '\u1D86': // ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] + fallthrough + case '\u1E3F': // ḿ [LATIN SMALL LETTER M WITH ACUTE] + fallthrough + case '\u1E41': // ṁ [LATIN SMALL LETTER M WITH DOT ABOVE] + fallthrough + case '\u1E43': // ṃ [LATIN SMALL LETTER M WITH DOT BELOW] + fallthrough + case '\u24DC': // ⓜ [CIRCLED LATIN SMALL LETTER M] + fallthrough + case '\uFF4D': // m [FULLWIDTH LATIN SMALL LETTER M] + output[outputPos] = 'm' + outputPos++ + + case '\u24A8': // ⒨ [PARENTHESIZED LATIN SMALL LETTER M] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'm' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00D1': // Ñ [LATIN CAPITAL LETTER N WITH TILDE] + fallthrough + case '\u0143': // Ń [LATIN CAPITAL LETTER N WITH ACUTE] + fallthrough + case '\u0145': // Ņ [LATIN CAPITAL LETTER N WITH CEDILLA] + fallthrough + case '\u0147': // Ň [LATIN CAPITAL LETTER N WITH CARON] + fallthrough + case '\u014A': // Ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] + fallthrough + case '\u019D': // Ɲ [LATIN CAPITAL LETTER N WITH LEFT HOOK] + fallthrough + case '\u01F8': // Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] + fallthrough + case '\u0220': // Ƞ [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] + fallthrough + case '\u0274': // ɴ [LATIN LETTER SMALL CAPITAL N] + fallthrough + case '\u1D0E': // ᴎ [LATIN LETTER SMALL CAPITAL REVERSED N] + fallthrough + case '\u1E44': // Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] + fallthrough + case '\u1E46': // Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] + fallthrough + case '\u1E48': // Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] + fallthrough + case '\u1E4A': // Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] + fallthrough + case '\u24C3': // Ⓝ [CIRCLED LATIN CAPITAL LETTER N] + fallthrough + case '\uFF2E': // N [FULLWIDTH LATIN CAPITAL LETTER N] + output[outputPos] = 'N' + outputPos++ + + case '\u00F1': // ñ [LATIN SMALL LETTER N WITH TILDE] + fallthrough + case '\u0144': // ń [LATIN SMALL LETTER N WITH ACUTE] + fallthrough + case '\u0146': // ņ [LATIN SMALL LETTER N WITH CEDILLA] + fallthrough + case '\u0148': // ň [LATIN SMALL LETTER N WITH CARON] + fallthrough + case '\u0149': // ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] + fallthrough + case '\u014B': // ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] + fallthrough + case '\u019E': // ƞ [LATIN SMALL LETTER N WITH LONG RIGHT LEG] + fallthrough + case '\u01F9': // ǹ [LATIN SMALL LETTER N WITH GRAVE] + fallthrough + case '\u0235': // ȵ [LATIN SMALL LETTER N WITH CURL] + fallthrough + case '\u0272': // ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] + fallthrough + case '\u0273': // ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] + fallthrough + case '\u1D70': // ᵰ [LATIN SMALL LETTER N WITH MIDDLE TILDE] + fallthrough + case '\u1D87': // ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] + fallthrough + case '\u1E45': // ṅ [LATIN SMALL LETTER N WITH DOT ABOVE] + fallthrough + case '\u1E47': // ṇ [LATIN SMALL LETTER N WITH DOT BELOW] + fallthrough + case '\u1E49': // ṉ [LATIN SMALL LETTER N WITH LINE BELOW] + fallthrough + case '\u1E4B': // ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] + fallthrough + case '\u207F': // ⁿ [SUPERSCRIPT LATIN SMALL LETTER N] + fallthrough + case '\u24DD': // ⓝ [CIRCLED LATIN SMALL LETTER N] + fallthrough + case '\uFF4E': // n [FULLWIDTH LATIN SMALL LETTER N] + output[outputPos] = 'n' + outputPos++ + + case '\u01CA': // NJ [LATIN CAPITAL LETTER NJ] + output = output[:(len(output) + 1)] + output[outputPos] = 'N' + outputPos++ + output[outputPos] = 'J' + outputPos++ + + case '\u01CB': // Nj [LATIN CAPITAL LETTER N WITH SMALL LETTER J] + output = output[:(len(output) + 1)] + output[outputPos] = 'N' + outputPos++ + output[outputPos] = 'j' + outputPos++ + + case '\u24A9': // ⒩ [PARENTHESIZED LATIN SMALL LETTER N] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'n' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u01CC': // nj [LATIN SMALL LETTER NJ] + output = output[:(len(output) + 1)] + output[outputPos] = 'n' + outputPos++ + output[outputPos] = 'j' + outputPos++ + + case '\u00D2': // Ò [LATIN CAPITAL LETTER O WITH GRAVE] + fallthrough + case '\u00D3': // Ó [LATIN CAPITAL LETTER O WITH ACUTE] + fallthrough + case '\u00D4': // Ô [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] + fallthrough + case '\u00D5': // Õ [LATIN CAPITAL LETTER O WITH TILDE] + fallthrough + case '\u00D6': // Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] + fallthrough + case '\u00D8': // Ø [LATIN CAPITAL LETTER O WITH STROKE] + fallthrough + case '\u014C': // Ō [LATIN CAPITAL LETTER O WITH MACRON] + fallthrough + case '\u014E': // Ŏ [LATIN CAPITAL LETTER O WITH BREVE] + fallthrough + case '\u0150': // Ő [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] + fallthrough + case '\u0186': // Ɔ [LATIN CAPITAL LETTER OPEN O] + fallthrough + case '\u019F': // Ɵ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] + fallthrough + case '\u01A0': // Ơ [LATIN CAPITAL LETTER O WITH HORN] + fallthrough + case '\u01D1': // Ǒ [LATIN CAPITAL LETTER O WITH CARON] + fallthrough + case '\u01EA': // Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] + fallthrough + case '\u01EC': // Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] + fallthrough + case '\u01FE': // Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] + fallthrough + case '\u020C': // Ȍ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] + fallthrough + case '\u020E': // Ȏ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] + fallthrough + case '\u022A': // Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] + fallthrough + case '\u022C': // Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] + fallthrough + case '\u022E': // Ȯ [LATIN CAPITAL LETTER O WITH DOT ABOVE] + fallthrough + case '\u0230': // Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] + fallthrough + case '\u1D0F': // ᴏ [LATIN LETTER SMALL CAPITAL O] + fallthrough + case '\u1D10': // ᴐ [LATIN LETTER SMALL CAPITAL OPEN O] + fallthrough + case '\u1E4C': // Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] + fallthrough + case '\u1E4E': // Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] + fallthrough + case '\u1E50': // Ṑ [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] + fallthrough + case '\u1E52': // Ṓ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] + fallthrough + case '\u1ECC': // Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] + fallthrough + case '\u1ECE': // Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] + fallthrough + case '\u1ED0': // Ố [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] + fallthrough + case '\u1ED2': // Ồ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] + fallthrough + case '\u1ED4': // Ổ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + fallthrough + case '\u1ED6': // Ỗ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] + fallthrough + case '\u1ED8': // Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + fallthrough + case '\u1EDA': // Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] + fallthrough + case '\u1EDC': // Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] + fallthrough + case '\u1EDE': // Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] + fallthrough + case '\u1EE0': // Ỡ [LATIN CAPITAL LETTER O WITH HORN AND TILDE] + fallthrough + case '\u1EE2': // Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] + fallthrough + case '\u24C4': // Ⓞ [CIRCLED LATIN CAPITAL LETTER O] + fallthrough + case '\uA74A': // Ꝋ [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] + fallthrough + case '\uA74C': // Ꝍ [LATIN CAPITAL LETTER O WITH LOOP] + fallthrough + case '\uFF2F': // O [FULLWIDTH LATIN CAPITAL LETTER O] + output[outputPos] = 'O' + outputPos++ + + case '\u00F2': // ò [LATIN SMALL LETTER O WITH GRAVE] + fallthrough + case '\u00F3': // ó [LATIN SMALL LETTER O WITH ACUTE] + fallthrough + case '\u00F4': // ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] + fallthrough + case '\u00F5': // õ [LATIN SMALL LETTER O WITH TILDE] + fallthrough + case '\u00F6': // ö [LATIN SMALL LETTER O WITH DIAERESIS] + fallthrough + case '\u00F8': // ø [LATIN SMALL LETTER O WITH STROKE] + fallthrough + case '\u014D': // ō [LATIN SMALL LETTER O WITH MACRON] + fallthrough + case '\u014F': // ŏ [LATIN SMALL LETTER O WITH BREVE] + fallthrough + case '\u0151': // ő [LATIN SMALL LETTER O WITH DOUBLE ACUTE] + fallthrough + case '\u01A1': // ơ [LATIN SMALL LETTER O WITH HORN] + fallthrough + case '\u01D2': // ǒ [LATIN SMALL LETTER O WITH CARON] + fallthrough + case '\u01EB': // ǫ [LATIN SMALL LETTER O WITH OGONEK] + fallthrough + case '\u01ED': // ǭ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] + fallthrough + case '\u01FF': // ǿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] + fallthrough + case '\u020D': // ȍ [LATIN SMALL LETTER O WITH DOUBLE GRAVE] + fallthrough + case '\u020F': // ȏ [LATIN SMALL LETTER O WITH INVERTED BREVE] + fallthrough + case '\u022B': // ȫ [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] + fallthrough + case '\u022D': // ȭ [LATIN SMALL LETTER O WITH TILDE AND MACRON] + fallthrough + case '\u022F': // ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] + fallthrough + case '\u0231': // ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] + fallthrough + case '\u0254': // ɔ [LATIN SMALL LETTER OPEN O] + fallthrough + case '\u0275': // ɵ [LATIN SMALL LETTER BARRED O] + fallthrough + case '\u1D16': // ᴖ [LATIN SMALL LETTER TOP HALF O] + fallthrough + case '\u1D17': // ᴗ [LATIN SMALL LETTER BOTTOM HALF O] + fallthrough + case '\u1D97': // ᶗ [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] + fallthrough + case '\u1E4D': // ṍ [LATIN SMALL LETTER O WITH TILDE AND ACUTE] + fallthrough + case '\u1E4F': // ṏ [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] + fallthrough + case '\u1E51': // ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] + fallthrough + case '\u1E53': // ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] + fallthrough + case '\u1ECD': // ọ [LATIN SMALL LETTER O WITH DOT BELOW] + fallthrough + case '\u1ECF': // ỏ [LATIN SMALL LETTER O WITH HOOK ABOVE] + fallthrough + case '\u1ED1': // ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] + fallthrough + case '\u1ED3': // ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] + fallthrough + case '\u1ED5': // ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] + fallthrough + case '\u1ED7': // ỗ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] + fallthrough + case '\u1ED9': // ộ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] + fallthrough + case '\u1EDB': // ớ [LATIN SMALL LETTER O WITH HORN AND ACUTE] + fallthrough + case '\u1EDD': // ờ [LATIN SMALL LETTER O WITH HORN AND GRAVE] + fallthrough + case '\u1EDF': // ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] + fallthrough + case '\u1EE1': // ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] + fallthrough + case '\u1EE3': // ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] + fallthrough + case '\u2092': // ₒ [LATIN SUBSCRIPT SMALL LETTER O] + fallthrough + case '\u24DE': // ⓞ [CIRCLED LATIN SMALL LETTER O] + fallthrough + case '\u2C7A': // ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] + fallthrough + case '\uA74B': // ꝋ [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] + fallthrough + case '\uA74D': // ꝍ [LATIN SMALL LETTER O WITH LOOP] + fallthrough + case '\uFF4F': // o [FULLWIDTH LATIN SMALL LETTER O] + output[outputPos] = 'o' + outputPos++ + + case '\u0152': // Œ [LATIN CAPITAL LIGATURE OE] + fallthrough + case '\u0276': // ɶ [LATIN LETTER SMALL CAPITAL OE] + output = output[:(len(output) + 1)] + output[outputPos] = 'O' + outputPos++ + output[outputPos] = 'E' + outputPos++ + + case '\uA74E': // Ꝏ [LATIN CAPITAL LETTER OO] + output = output[:(len(output) + 1)] + output[outputPos] = 'O' + outputPos++ + output[outputPos] = 'O' + outputPos++ + + case '\u0222': // Ȣ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] + fallthrough + case '\u1D15': // ᴕ [LATIN LETTER SMALL CAPITAL OU] + output = output[:(len(output) + 1)] + output[outputPos] = 'O' + outputPos++ + output[outputPos] = 'U' + outputPos++ + + case '\u24AA': // ⒪ [PARENTHESIZED LATIN SMALL LETTER O] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'o' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0153': // œ [LATIN SMALL LIGATURE OE] + fallthrough + case '\u1D14': // ᴔ [LATIN SMALL LETTER TURNED OE] + output = output[:(len(output) + 1)] + output[outputPos] = 'o' + outputPos++ + output[outputPos] = 'e' + outputPos++ + + case '\uA74F': // ꝏ [LATIN SMALL LETTER OO] + output = output[:(len(output) + 1)] + output[outputPos] = 'o' + outputPos++ + output[outputPos] = 'o' + outputPos++ + + case '\u0223': // ȣ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] + output = output[:(len(output) + 1)] + output[outputPos] = 'o' + outputPos++ + output[outputPos] = 'u' + outputPos++ + + case '\u01A4': // Ƥ [LATIN CAPITAL LETTER P WITH HOOK] + fallthrough + case '\u1D18': // ᴘ [LATIN LETTER SMALL CAPITAL P] + fallthrough + case '\u1E54': // Ṕ [LATIN CAPITAL LETTER P WITH ACUTE] + fallthrough + case '\u1E56': // Ṗ [LATIN CAPITAL LETTER P WITH DOT ABOVE] + fallthrough + case '\u24C5': // Ⓟ [CIRCLED LATIN CAPITAL LETTER P] + fallthrough + case '\u2C63': // Ᵽ [LATIN CAPITAL LETTER P WITH STROKE] + fallthrough + case '\uA750': // Ꝑ [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] + fallthrough + case '\uA752': // Ꝓ [LATIN CAPITAL LETTER P WITH FLOURISH] + fallthrough + case '\uA754': // Ꝕ [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] + fallthrough + case '\uFF30': // P [FULLWIDTH LATIN CAPITAL LETTER P] + output[outputPos] = 'P' + outputPos++ + + case '\u01A5': // ƥ [LATIN SMALL LETTER P WITH HOOK] + fallthrough + case '\u1D71': // ᵱ [LATIN SMALL LETTER P WITH MIDDLE TILDE] + fallthrough + case '\u1D7D': // ᵽ [LATIN SMALL LETTER P WITH STROKE] + fallthrough + case '\u1D88': // ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] + fallthrough + case '\u1E55': // ṕ [LATIN SMALL LETTER P WITH ACUTE] + fallthrough + case '\u1E57': // ṗ [LATIN SMALL LETTER P WITH DOT ABOVE] + fallthrough + case '\u24DF': // ⓟ [CIRCLED LATIN SMALL LETTER P] + fallthrough + case '\uA751': // ꝑ [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] + fallthrough + case '\uA753': // ꝓ [LATIN SMALL LETTER P WITH FLOURISH] + fallthrough + case '\uA755': // ꝕ [LATIN SMALL LETTER P WITH SQUIRREL TAIL] + fallthrough + case '\uA7FC': // ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] + fallthrough + case '\uFF50': // p [FULLWIDTH LATIN SMALL LETTER P] + output[outputPos] = 'p' + outputPos++ + + case '\u24AB': // ⒫ [PARENTHESIZED LATIN SMALL LETTER P] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'p' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u024A': // Ɋ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] + fallthrough + case '\u24C6': // Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] + fallthrough + case '\uA756': // Ꝗ [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] + fallthrough + case '\uA758': // Ꝙ [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] + fallthrough + case '\uFF31': // Q [FULLWIDTH LATIN CAPITAL LETTER Q] + output[outputPos] = 'Q' + outputPos++ + + case '\u0138': // ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] + fallthrough + case '\u024B': // ɋ [LATIN SMALL LETTER Q WITH HOOK TAIL] + fallthrough + case '\u02A0': // ʠ [LATIN SMALL LETTER Q WITH HOOK] + fallthrough + case '\u24E0': // ⓠ [CIRCLED LATIN SMALL LETTER Q] + fallthrough + case '\uA757': // ꝗ [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] + fallthrough + case '\uA759': // ꝙ [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] + fallthrough + case '\uFF51': // q [FULLWIDTH LATIN SMALL LETTER Q] + output[outputPos] = 'q' + outputPos++ + + case '\u24AC': // ⒬ [PARENTHESIZED LATIN SMALL LETTER Q] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'q' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0239': // ȹ [LATIN SMALL LETTER QP DIGRAPH] + output = output[:(len(output) + 1)] + output[outputPos] = 'q' + outputPos++ + output[outputPos] = 'p' + outputPos++ + + case '\u0154': // Ŕ [LATIN CAPITAL LETTER R WITH ACUTE] + fallthrough + case '\u0156': // Ŗ [LATIN CAPITAL LETTER R WITH CEDILLA] + fallthrough + case '\u0158': // Ř [LATIN CAPITAL LETTER R WITH CARON] + fallthrough + case '\u0210': // Ȓ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] + fallthrough + case '\u0212': // Ȓ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] + fallthrough + case '\u024C': // Ɍ [LATIN CAPITAL LETTER R WITH STROKE] + fallthrough + case '\u0280': // ʀ [LATIN LETTER SMALL CAPITAL R] + fallthrough + case '\u0281': // ʁ [LATIN LETTER SMALL CAPITAL INVERTED R] + fallthrough + case '\u1D19': // ᴙ [LATIN LETTER SMALL CAPITAL REVERSED R] + fallthrough + case '\u1D1A': // ᴚ [LATIN LETTER SMALL CAPITAL TURNED R] + fallthrough + case '\u1E58': // Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] + fallthrough + case '\u1E5A': // Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] + fallthrough + case '\u1E5C': // Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] + fallthrough + case '\u1E5E': // Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] + fallthrough + case '\u24C7': // Ⓡ [CIRCLED LATIN CAPITAL LETTER R] + fallthrough + case '\u2C64': // Ɽ [LATIN CAPITAL LETTER R WITH TAIL] + fallthrough + case '\uA75A': // Ꝛ [LATIN CAPITAL LETTER R ROTUNDA] + fallthrough + case '\uA782': // Ꞃ [LATIN CAPITAL LETTER INSULAR R] + fallthrough + case '\uFF32': // R [FULLWIDTH LATIN CAPITAL LETTER R] + output[outputPos] = 'R' + outputPos++ + + case '\u0155': // ŕ [LATIN SMALL LETTER R WITH ACUTE] + fallthrough + case '\u0157': // ŗ [LATIN SMALL LETTER R WITH CEDILLA] + fallthrough + case '\u0159': // ř [LATIN SMALL LETTER R WITH CARON] + fallthrough + case '\u0211': // ȑ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] + fallthrough + case '\u0213': // ȓ [LATIN SMALL LETTER R WITH INVERTED BREVE] + fallthrough + case '\u024D': // ɍ [LATIN SMALL LETTER R WITH STROKE] + fallthrough + case '\u027C': // ɼ [LATIN SMALL LETTER R WITH LONG LEG] + fallthrough + case '\u027D': // ɽ [LATIN SMALL LETTER R WITH TAIL] + fallthrough + case '\u027E': // ɾ [LATIN SMALL LETTER R WITH FISHHOOK] + fallthrough + case '\u027F': // ɿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] + fallthrough + case '\u1D63': // ᵣ [LATIN SUBSCRIPT SMALL LETTER R] + fallthrough + case '\u1D72': // ᵲ [LATIN SMALL LETTER R WITH MIDDLE TILDE] + fallthrough + case '\u1D73': // ᵳ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] + fallthrough + case '\u1D89': // ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] + fallthrough + case '\u1E59': // ṙ [LATIN SMALL LETTER R WITH DOT ABOVE] + fallthrough + case '\u1E5B': // ṛ [LATIN SMALL LETTER R WITH DOT BELOW] + fallthrough + case '\u1E5D': // ṝ [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] + fallthrough + case '\u1E5F': // ṟ [LATIN SMALL LETTER R WITH LINE BELOW] + fallthrough + case '\u24E1': // ⓡ [CIRCLED LATIN SMALL LETTER R] + fallthrough + case '\uA75B': // ꝛ [LATIN SMALL LETTER R ROTUNDA] + fallthrough + case '\uA783': // ꞃ [LATIN SMALL LETTER INSULAR R] + fallthrough + case '\uFF52': // r [FULLWIDTH LATIN SMALL LETTER R] + output[outputPos] = 'r' + outputPos++ + + case '\u24AD': // ⒭ [PARENTHESIZED LATIN SMALL LETTER R] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'r' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u015A': // Ś [LATIN CAPITAL LETTER S WITH ACUTE] + fallthrough + case '\u015C': // Ŝ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] + fallthrough + case '\u015E': // Ş [LATIN CAPITAL LETTER S WITH CEDILLA] + fallthrough + case '\u0160': // Š [LATIN CAPITAL LETTER S WITH CARON] + fallthrough + case '\u0218': // Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] + fallthrough + case '\u1E60': // Ṡ [LATIN CAPITAL LETTER S WITH DOT ABOVE] + fallthrough + case '\u1E62': // Ṣ [LATIN CAPITAL LETTER S WITH DOT BELOW] + fallthrough + case '\u1E64': // Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] + fallthrough + case '\u1E66': // Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] + fallthrough + case '\u1E68': // Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] + fallthrough + case '\u24C8': // Ⓢ [CIRCLED LATIN CAPITAL LETTER S] + fallthrough + case '\uA731': // ꜱ [LATIN LETTER SMALL CAPITAL S] + fallthrough + case '\uA785': // ꞅ [LATIN SMALL LETTER INSULAR S] + fallthrough + case '\uFF33': // S [FULLWIDTH LATIN CAPITAL LETTER S] + output[outputPos] = 'S' + outputPos++ + + case '\u015B': // ś [LATIN SMALL LETTER S WITH ACUTE] + fallthrough + case '\u015D': // ŝ [LATIN SMALL LETTER S WITH CIRCUMFLEX] + fallthrough + case '\u015F': // ş [LATIN SMALL LETTER S WITH CEDILLA] + fallthrough + case '\u0161': // š [LATIN SMALL LETTER S WITH CARON] + fallthrough + case '\u017F': // ſ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] + fallthrough + case '\u0219': // ș [LATIN SMALL LETTER S WITH COMMA BELOW] + fallthrough + case '\u023F': // ȿ [LATIN SMALL LETTER S WITH SWASH TAIL] + fallthrough + case '\u0282': // ʂ [LATIN SMALL LETTER S WITH HOOK] + fallthrough + case '\u1D74': // ᵴ [LATIN SMALL LETTER S WITH MIDDLE TILDE] + fallthrough + case '\u1D8A': // ᶊ [LATIN SMALL LETTER S WITH PALATAL HOOK] + fallthrough + case '\u1E61': // ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] + fallthrough + case '\u1E63': // ṣ [LATIN SMALL LETTER S WITH DOT BELOW] + fallthrough + case '\u1E65': // ṥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] + fallthrough + case '\u1E67': // ṧ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] + fallthrough + case '\u1E69': // ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] + fallthrough + case '\u1E9C': // ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] + fallthrough + case '\u1E9D': // ẝ [LATIN SMALL LETTER LONG S WITH HIGH STROKE] + fallthrough + case '\u24E2': // ⓢ [CIRCLED LATIN SMALL LETTER S] + fallthrough + case '\uA784': // Ꞅ [LATIN CAPITAL LETTER INSULAR S] + fallthrough + case '\uFF53': // s [FULLWIDTH LATIN SMALL LETTER S] + output[outputPos] = 's' + outputPos++ + + case '\u1E9E': // ẞ [LATIN CAPITAL LETTER SHARP S] + output = output[:(len(output) + 1)] + output[outputPos] = 'S' + outputPos++ + output[outputPos] = 'S' + outputPos++ + + case '\u24AE': // ⒮ [PARENTHESIZED LATIN SMALL LETTER S] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 's' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00DF': // ß [LATIN SMALL LETTER SHARP S] + output = output[:(len(output) + 1)] + output[outputPos] = 's' + outputPos++ + output[outputPos] = 's' + outputPos++ + + case '\uFB06': // st [LATIN SMALL LIGATURE ST] + output = output[:(len(output) + 1)] + output[outputPos] = 's' + outputPos++ + output[outputPos] = 't' + outputPos++ + + case '\u0162': // Ţ [LATIN CAPITAL LETTER T WITH CEDILLA] + fallthrough + case '\u0164': // Ť [LATIN CAPITAL LETTER T WITH CARON] + fallthrough + case '\u0166': // Ŧ [LATIN CAPITAL LETTER T WITH STROKE] + fallthrough + case '\u01AC': // Ƭ [LATIN CAPITAL LETTER T WITH HOOK] + fallthrough + case '\u01AE': // Ʈ [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] + fallthrough + case '\u021A': // Ț [LATIN CAPITAL LETTER T WITH COMMA BELOW] + fallthrough + case '\u023E': // Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] + fallthrough + case '\u1D1B': // ᴛ [LATIN LETTER SMALL CAPITAL T] + fallthrough + case '\u1E6A': // Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] + fallthrough + case '\u1E6C': // Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] + fallthrough + case '\u1E6E': // Ṯ [LATIN CAPITAL LETTER T WITH LINE BELOW] + fallthrough + case '\u1E70': // Ṱ [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] + fallthrough + case '\u24C9': // Ⓣ [CIRCLED LATIN CAPITAL LETTER T] + fallthrough + case '\uA786': // Ꞇ [LATIN CAPITAL LETTER INSULAR T] + fallthrough + case '\uFF34': // T [FULLWIDTH LATIN CAPITAL LETTER T] + output[outputPos] = 'T' + outputPos++ + + case '\u0163': // ţ [LATIN SMALL LETTER T WITH CEDILLA] + fallthrough + case '\u0165': // ť [LATIN SMALL LETTER T WITH CARON] + fallthrough + case '\u0167': // ŧ [LATIN SMALL LETTER T WITH STROKE] + fallthrough + case '\u01AB': // ƫ [LATIN SMALL LETTER T WITH PALATAL HOOK] + fallthrough + case '\u01AD': // ƭ [LATIN SMALL LETTER T WITH HOOK] + fallthrough + case '\u021B': // ț [LATIN SMALL LETTER T WITH COMMA BELOW] + fallthrough + case '\u0236': // ȶ [LATIN SMALL LETTER T WITH CURL] + fallthrough + case '\u0287': // ʇ [LATIN SMALL LETTER TURNED T] + fallthrough + case '\u0288': // ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] + fallthrough + case '\u1D75': // ᵵ [LATIN SMALL LETTER T WITH MIDDLE TILDE] + fallthrough + case '\u1E6B': // ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] + fallthrough + case '\u1E6D': // ṭ [LATIN SMALL LETTER T WITH DOT BELOW] + fallthrough + case '\u1E6F': // ṯ [LATIN SMALL LETTER T WITH LINE BELOW] + fallthrough + case '\u1E71': // ṱ [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] + fallthrough + case '\u1E97': // ẗ [LATIN SMALL LETTER T WITH DIAERESIS] + fallthrough + case '\u24E3': // ⓣ [CIRCLED LATIN SMALL LETTER T] + fallthrough + case '\u2C66': // ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] + fallthrough + case '\uFF54': // t [FULLWIDTH LATIN SMALL LETTER T] + output[outputPos] = 't' + outputPos++ + + case '\u00DE': // Þ [LATIN CAPITAL LETTER THORN] + fallthrough + case '\uA766': // Ꝧ [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] + output = output[:(len(output) + 1)] + output[outputPos] = 'T' + outputPos++ + output[outputPos] = 'H' + outputPos++ + + case '\uA728': // Ꜩ [LATIN CAPITAL LETTER TZ] + output = output[:(len(output) + 1)] + output[outputPos] = 'T' + outputPos++ + output[outputPos] = 'Z' + outputPos++ + + case '\u24AF': // ⒯ [PARENTHESIZED LATIN SMALL LETTER T] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 't' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u02A8': // ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] + output = output[:(len(output) + 1)] + output[outputPos] = 't' + outputPos++ + output[outputPos] = 'c' + outputPos++ + + case '\u00FE': // þ [LATIN SMALL LETTER THORN] + fallthrough + case '\u1D7A': // ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] + fallthrough + case '\uA767': // ꝧ [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] + output = output[:(len(output) + 1)] + output[outputPos] = 't' + outputPos++ + output[outputPos] = 'h' + outputPos++ + + case '\u02A6': // ʦ [LATIN SMALL LETTER TS DIGRAPH] + output = output[:(len(output) + 1)] + output[outputPos] = 't' + outputPos++ + output[outputPos] = 's' + outputPos++ + + case '\uA729': // ꜩ [LATIN SMALL LETTER TZ] + output = output[:(len(output) + 1)] + output[outputPos] = 't' + outputPos++ + output[outputPos] = 'z' + outputPos++ + + case '\u00D9': // Ù [LATIN CAPITAL LETTER U WITH GRAVE] + fallthrough + case '\u00DA': // Ú [LATIN CAPITAL LETTER U WITH ACUTE] + fallthrough + case '\u00DB': // Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] + fallthrough + case '\u00DC': // Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] + fallthrough + case '\u0168': // Ũ [LATIN CAPITAL LETTER U WITH TILDE] + fallthrough + case '\u016A': // Ū [LATIN CAPITAL LETTER U WITH MACRON] + fallthrough + case '\u016C': // Ŭ [LATIN CAPITAL LETTER U WITH BREVE] + fallthrough + case '\u016E': // Ů [LATIN CAPITAL LETTER U WITH RING ABOVE] + fallthrough + case '\u0170': // Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] + fallthrough + case '\u0172': // Ų [LATIN CAPITAL LETTER U WITH OGONEK] + fallthrough + case '\u01AF': // Ư [LATIN CAPITAL LETTER U WITH HORN] + fallthrough + case '\u01D3': // Ǔ [LATIN CAPITAL LETTER U WITH CARON] + fallthrough + case '\u01D5': // Ǖ [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] + fallthrough + case '\u01D7': // Ǘ [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] + fallthrough + case '\u01D9': // Ǚ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] + fallthrough + case '\u01DB': // Ǜ [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] + fallthrough + case '\u0214': // Ȕ [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] + fallthrough + case '\u0216': // Ȗ [LATIN CAPITAL LETTER U WITH INVERTED BREVE] + fallthrough + case '\u0244': // Ʉ [LATIN CAPITAL LETTER U BAR] + fallthrough + case '\u1D1C': // ᴜ [LATIN LETTER SMALL CAPITAL U] + fallthrough + case '\u1D7E': // ᵾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] + fallthrough + case '\u1E72': // Ṳ [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] + fallthrough + case '\u1E74': // Ṵ [LATIN CAPITAL LETTER U WITH TILDE BELOW] + fallthrough + case '\u1E76': // Ṷ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] + fallthrough + case '\u1E78': // Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] + fallthrough + case '\u1E7A': // Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] + fallthrough + case '\u1EE4': // Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] + fallthrough + case '\u1EE6': // Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] + fallthrough + case '\u1EE8': // Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] + fallthrough + case '\u1EEA': // Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] + fallthrough + case '\u1EEC': // Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] + fallthrough + case '\u1EEE': // Ữ [LATIN CAPITAL LETTER U WITH HORN AND TILDE] + fallthrough + case '\u1EF0': // Ự [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] + fallthrough + case '\u24CA': // Ⓤ [CIRCLED LATIN CAPITAL LETTER U] + fallthrough + case '\uFF35': // U [FULLWIDTH LATIN CAPITAL LETTER U] + output[outputPos] = 'U' + outputPos++ + + case '\u00F9': // ù [LATIN SMALL LETTER U WITH GRAVE] + fallthrough + case '\u00FA': // ú [LATIN SMALL LETTER U WITH ACUTE] + fallthrough + case '\u00FB': // û [LATIN SMALL LETTER U WITH CIRCUMFLEX] + fallthrough + case '\u00FC': // ü [LATIN SMALL LETTER U WITH DIAERESIS] + fallthrough + case '\u0169': // ũ [LATIN SMALL LETTER U WITH TILDE] + fallthrough + case '\u016B': // ū [LATIN SMALL LETTER U WITH MACRON] + fallthrough + case '\u016D': // ŭ [LATIN SMALL LETTER U WITH BREVE] + fallthrough + case '\u016F': // ů [LATIN SMALL LETTER U WITH RING ABOVE] + fallthrough + case '\u0171': // ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] + fallthrough + case '\u0173': // ų [LATIN SMALL LETTER U WITH OGONEK] + fallthrough + case '\u01B0': // ư [LATIN SMALL LETTER U WITH HORN] + fallthrough + case '\u01D4': // ǔ [LATIN SMALL LETTER U WITH CARON] + fallthrough + case '\u01D6': // ǖ [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] + fallthrough + case '\u01D8': // ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] + fallthrough + case '\u01DA': // ǚ [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] + fallthrough + case '\u01DC': // ǜ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] + fallthrough + case '\u0215': // ȕ [LATIN SMALL LETTER U WITH DOUBLE GRAVE] + fallthrough + case '\u0217': // ȗ [LATIN SMALL LETTER U WITH INVERTED BREVE] + fallthrough + case '\u0289': // ʉ [LATIN SMALL LETTER U BAR] + fallthrough + case '\u1D64': // ᵤ [LATIN SUBSCRIPT SMALL LETTER U] + fallthrough + case '\u1D99': // ᶙ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] + fallthrough + case '\u1E73': // ṳ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] + fallthrough + case '\u1E75': // ṵ [LATIN SMALL LETTER U WITH TILDE BELOW] + fallthrough + case '\u1E77': // ṷ [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] + fallthrough + case '\u1E79': // ṹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] + fallthrough + case '\u1E7B': // ṻ [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] + fallthrough + case '\u1EE5': // ụ [LATIN SMALL LETTER U WITH DOT BELOW] + fallthrough + case '\u1EE7': // ủ [LATIN SMALL LETTER U WITH HOOK ABOVE] + fallthrough + case '\u1EE9': // ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] + fallthrough + case '\u1EEB': // ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] + fallthrough + case '\u1EED': // ử [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] + fallthrough + case '\u1EEF': // ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] + fallthrough + case '\u1EF1': // ự [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] + fallthrough + case '\u24E4': // ⓤ [CIRCLED LATIN SMALL LETTER U] + fallthrough + case '\uFF55': // u [FULLWIDTH LATIN SMALL LETTER U] + output[outputPos] = 'u' + outputPos++ + + case '\u24B0': // ⒰ [PARENTHESIZED LATIN SMALL LETTER U] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'u' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u1D6B': // ᵫ [LATIN SMALL LETTER UE] + output = output[:(len(output) + 1)] + output[outputPos] = 'u' + outputPos++ + output[outputPos] = 'e' + outputPos++ + + case '\u01B2': // Ʋ [LATIN CAPITAL LETTER V WITH HOOK] + fallthrough + case '\u0245': // Ʌ [LATIN CAPITAL LETTER TURNED V] + fallthrough + case '\u1D20': // ᴠ [LATIN LETTER SMALL CAPITAL V] + fallthrough + case '\u1E7C': // Ṽ [LATIN CAPITAL LETTER V WITH TILDE] + fallthrough + case '\u1E7E': // Ṿ [LATIN CAPITAL LETTER V WITH DOT BELOW] + fallthrough + case '\u1EFC': // Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] + fallthrough + case '\u24CB': // Ⓥ [CIRCLED LATIN CAPITAL LETTER V] + fallthrough + case '\uA75E': // Ꝟ [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] + fallthrough + case '\uA768': // Ꝩ [LATIN CAPITAL LETTER VEND] + fallthrough + case '\uFF36': // V [FULLWIDTH LATIN CAPITAL LETTER V] + output[outputPos] = 'V' + outputPos++ + + case '\u028B': // ʋ [LATIN SMALL LETTER V WITH HOOK] + fallthrough + case '\u028C': // ʌ [LATIN SMALL LETTER TURNED V] + fallthrough + case '\u1D65': // ᵥ [LATIN SUBSCRIPT SMALL LETTER V] + fallthrough + case '\u1D8C': // ᶌ [LATIN SMALL LETTER V WITH PALATAL HOOK] + fallthrough + case '\u1E7D': // ṽ [LATIN SMALL LETTER V WITH TILDE] + fallthrough + case '\u1E7F': // ṿ [LATIN SMALL LETTER V WITH DOT BELOW] + fallthrough + case '\u24E5': // ⓥ [CIRCLED LATIN SMALL LETTER V] + fallthrough + case '\u2C71': // ⱱ [LATIN SMALL LETTER V WITH RIGHT HOOK] + fallthrough + case '\u2C74': // ⱴ [LATIN SMALL LETTER V WITH CURL] + fallthrough + case '\uA75F': // ꝟ [LATIN SMALL LETTER V WITH DIAGONAL STROKE] + fallthrough + case '\uFF56': // v [FULLWIDTH LATIN SMALL LETTER V] + output[outputPos] = 'v' + outputPos++ + + case '\uA760': // Ꝡ [LATIN CAPITAL LETTER VY] + output = output[:(len(output) + 1)] + output[outputPos] = 'V' + outputPos++ + output[outputPos] = 'Y' + outputPos++ + + case '\u24B1': // ⒱ [PARENTHESIZED LATIN SMALL LETTER V] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'v' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\uA761': // ꝡ [LATIN SMALL LETTER VY] + output = output[:(len(output) + 1)] + output[outputPos] = 'v' + outputPos++ + output[outputPos] = 'y' + outputPos++ + + case '\u0174': // Ŵ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] + fallthrough + case '\u01F7': // Ƿ http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] + fallthrough + case '\u1D21': // ᴡ [LATIN LETTER SMALL CAPITAL W] + fallthrough + case '\u1E80': // Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] + fallthrough + case '\u1E82': // Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] + fallthrough + case '\u1E84': // Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] + fallthrough + case '\u1E86': // Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] + fallthrough + case '\u1E88': // Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] + fallthrough + case '\u24CC': // Ⓦ [CIRCLED LATIN CAPITAL LETTER W] + fallthrough + case '\u2C72': // Ⱳ [LATIN CAPITAL LETTER W WITH HOOK] + fallthrough + case '\uFF37': // W [FULLWIDTH LATIN CAPITAL LETTER W] + output[outputPos] = 'W' + outputPos++ + + case '\u0175': // ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] + fallthrough + case '\u01BF': // ƿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] + fallthrough + case '\u028D': // ʍ [LATIN SMALL LETTER TURNED W] + fallthrough + case '\u1E81': // ẁ [LATIN SMALL LETTER W WITH GRAVE] + fallthrough + case '\u1E83': // ẃ [LATIN SMALL LETTER W WITH ACUTE] + fallthrough + case '\u1E85': // ẅ [LATIN SMALL LETTER W WITH DIAERESIS] + fallthrough + case '\u1E87': // ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] + fallthrough + case '\u1E89': // ẉ [LATIN SMALL LETTER W WITH DOT BELOW] + fallthrough + case '\u1E98': // ẘ [LATIN SMALL LETTER W WITH RING ABOVE] + fallthrough + case '\u24E6': // ⓦ [CIRCLED LATIN SMALL LETTER W] + fallthrough + case '\u2C73': // ⱳ [LATIN SMALL LETTER W WITH HOOK] + fallthrough + case '\uFF57': // w [FULLWIDTH LATIN SMALL LETTER W] + output[outputPos] = 'w' + outputPos++ + + case '\u24B2': // ⒲ [PARENTHESIZED LATIN SMALL LETTER W] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'w' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u1E8A': // Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] + fallthrough + case '\u1E8C': // Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] + fallthrough + case '\u24CD': // Ⓧ [CIRCLED LATIN CAPITAL LETTER X] + fallthrough + case '\uFF38': // X [FULLWIDTH LATIN CAPITAL LETTER X] + output[outputPos] = 'X' + outputPos++ + + case '\u1D8D': // ᶍ [LATIN SMALL LETTER X WITH PALATAL HOOK] + fallthrough + case '\u1E8B': // ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] + fallthrough + case '\u1E8D': // ẍ [LATIN SMALL LETTER X WITH DIAERESIS] + fallthrough + case '\u2093': // ₓ [LATIN SUBSCRIPT SMALL LETTER X] + fallthrough + case '\u24E7': // ⓧ [CIRCLED LATIN SMALL LETTER X] + fallthrough + case '\uFF58': // x [FULLWIDTH LATIN SMALL LETTER X] + output[outputPos] = 'x' + outputPos++ + + case '\u24B3': // ⒳ [PARENTHESIZED LATIN SMALL LETTER X] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'x' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00DD': // Ý [LATIN CAPITAL LETTER Y WITH ACUTE] + fallthrough + case '\u0176': // Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] + fallthrough + case '\u0178': // Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] + fallthrough + case '\u01B3': // Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] + fallthrough + case '\u0232': // Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] + fallthrough + case '\u024E': // Ɏ [LATIN CAPITAL LETTER Y WITH STROKE] + fallthrough + case '\u028F': // ʏ [LATIN LETTER SMALL CAPITAL Y] + fallthrough + case '\u1E8E': // Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] + fallthrough + case '\u1EF2': // Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] + fallthrough + case '\u1EF4': // Ỵ [LATIN CAPITAL LETTER Y WITH DOT BELOW] + fallthrough + case '\u1EF6': // Ỷ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] + fallthrough + case '\u1EF8': // Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] + fallthrough + case '\u1EFE': // Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] + fallthrough + case '\u24CE': // Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] + fallthrough + case '\uFF39': // Y [FULLWIDTH LATIN CAPITAL LETTER Y] + output[outputPos] = 'Y' + outputPos++ + + case '\u00FD': // ý [LATIN SMALL LETTER Y WITH ACUTE] + fallthrough + case '\u00FF': // ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] + fallthrough + case '\u0177': // ŷ [LATIN SMALL LETTER Y WITH CIRCUMFLEX] + fallthrough + case '\u01B4': // ƴ [LATIN SMALL LETTER Y WITH HOOK] + fallthrough + case '\u0233': // ȳ [LATIN SMALL LETTER Y WITH MACRON] + fallthrough + case '\u024F': // ɏ [LATIN SMALL LETTER Y WITH STROKE] + fallthrough + case '\u028E': // ʎ [LATIN SMALL LETTER TURNED Y] + fallthrough + case '\u1E8F': // ẏ [LATIN SMALL LETTER Y WITH DOT ABOVE] + fallthrough + case '\u1E99': // ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] + fallthrough + case '\u1EF3': // ỳ [LATIN SMALL LETTER Y WITH GRAVE] + fallthrough + case '\u1EF5': // ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] + fallthrough + case '\u1EF7': // ỷ [LATIN SMALL LETTER Y WITH HOOK ABOVE] + fallthrough + case '\u1EF9': // ỹ [LATIN SMALL LETTER Y WITH TILDE] + fallthrough + case '\u1EFF': // ỿ [LATIN SMALL LETTER Y WITH LOOP] + fallthrough + case '\u24E8': // ⓨ [CIRCLED LATIN SMALL LETTER Y] + fallthrough + case '\uFF59': // y [FULLWIDTH LATIN SMALL LETTER Y] + output[outputPos] = 'y' + outputPos++ + + case '\u24B4': // ⒴ [PARENTHESIZED LATIN SMALL LETTER Y] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'y' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u0179': // Ź [LATIN CAPITAL LETTER Z WITH ACUTE] + fallthrough + case '\u017B': // Ż [LATIN CAPITAL LETTER Z WITH DOT ABOVE] + fallthrough + case '\u017D': // Ž [LATIN CAPITAL LETTER Z WITH CARON] + fallthrough + case '\u01B5': // Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] + fallthrough + case '\u021C': // Ȝ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] + fallthrough + case '\u0224': // Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] + fallthrough + case '\u1D22': // ᴢ [LATIN LETTER SMALL CAPITAL Z] + fallthrough + case '\u1E90': // Ẑ [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] + fallthrough + case '\u1E92': // Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] + fallthrough + case '\u1E94': // Ẕ [LATIN CAPITAL LETTER Z WITH LINE BELOW] + fallthrough + case '\u24CF': // Ⓩ [CIRCLED LATIN CAPITAL LETTER Z] + fallthrough + case '\u2C6B': // Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] + fallthrough + case '\uA762': // Ꝣ [LATIN CAPITAL LETTER VISIGOTHIC Z] + fallthrough + case '\uFF3A': // Z [FULLWIDTH LATIN CAPITAL LETTER Z] + output[outputPos] = 'Z' + outputPos++ + + case '\u017A': // ź [LATIN SMALL LETTER Z WITH ACUTE] + fallthrough + case '\u017C': // ż [LATIN SMALL LETTER Z WITH DOT ABOVE] + fallthrough + case '\u017E': // ž [LATIN SMALL LETTER Z WITH CARON] + fallthrough + case '\u01B6': // ƶ [LATIN SMALL LETTER Z WITH STROKE] + fallthrough + case '\u021D': // ȝ http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] + fallthrough + case '\u0225': // ȥ [LATIN SMALL LETTER Z WITH HOOK] + fallthrough + case '\u0240': // ɀ [LATIN SMALL LETTER Z WITH SWASH TAIL] + fallthrough + case '\u0290': // ʐ [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] + fallthrough + case '\u0291': // ʑ [LATIN SMALL LETTER Z WITH CURL] + fallthrough + case '\u1D76': // ᵶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] + fallthrough + case '\u1D8E': // ᶎ [LATIN SMALL LETTER Z WITH PALATAL HOOK] + fallthrough + case '\u1E91': // ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] + fallthrough + case '\u1E93': // ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] + fallthrough + case '\u1E95': // ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] + fallthrough + case '\u24E9': // ⓩ [CIRCLED LATIN SMALL LETTER Z] + fallthrough + case '\u2C6C': // ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] + fallthrough + case '\uA763': // ꝣ [LATIN SMALL LETTER VISIGOTHIC Z] + fallthrough + case '\uFF5A': // z [FULLWIDTH LATIN SMALL LETTER Z] + output[outputPos] = 'z' + outputPos++ + + case '\u24B5': // ⒵ [PARENTHESIZED LATIN SMALL LETTER Z] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = 'z' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2070': // ⁰ [SUPERSCRIPT ZERO] + fallthrough + case '\u2080': // ₀ [SUBSCRIPT ZERO] + fallthrough + case '\u24EA': // ⓪ [CIRCLED DIGIT ZERO] + fallthrough + case '\u24FF': // ⓿ [NEGATIVE CIRCLED DIGIT ZERO] + fallthrough + case '\uFF10': // 0 [FULLWIDTH DIGIT ZERO] + output[outputPos] = '0' + outputPos++ + + case '\u00B9': // ¹ [SUPERSCRIPT ONE] + fallthrough + case '\u2081': // ₁ [SUBSCRIPT ONE] + fallthrough + case '\u2460': // ① [CIRCLED DIGIT ONE] + fallthrough + case '\u24F5': // ⓵ [DOUBLE CIRCLED DIGIT ONE] + fallthrough + case '\u2776': // ❶ [DINGBAT NEGATIVE CIRCLED DIGIT ONE] + fallthrough + case '\u2780': // ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] + fallthrough + case '\u278A': // ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] + fallthrough + case '\uFF11': // 1 [FULLWIDTH DIGIT ONE] + output[outputPos] = '1' + outputPos++ + + case '\u2488': // ⒈ [DIGIT ONE FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2474': // ⑴ [PARENTHESIZED DIGIT ONE] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00B2': // ² [SUPERSCRIPT TWO] + fallthrough + case '\u2082': // ₂ [SUBSCRIPT TWO] + fallthrough + case '\u2461': // ② [CIRCLED DIGIT TWO] + fallthrough + case '\u24F6': // ⓶ [DOUBLE CIRCLED DIGIT TWO] + fallthrough + case '\u2777': // ❷ [DINGBAT NEGATIVE CIRCLED DIGIT TWO] + fallthrough + case '\u2781': // ➁ [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] + fallthrough + case '\u278B': // ➋ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] + fallthrough + case '\uFF12': // 2 [FULLWIDTH DIGIT TWO] + output[outputPos] = '2' + outputPos++ + + case '\u2489': // ⒉ [DIGIT TWO FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '2' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2475': // ⑵ [PARENTHESIZED DIGIT TWO] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '2' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00B3': // ³ [SUPERSCRIPT THREE] + fallthrough + case '\u2083': // ₃ [SUBSCRIPT THREE] + fallthrough + case '\u2462': // ③ [CIRCLED DIGIT THREE] + fallthrough + case '\u24F7': // ⓷ [DOUBLE CIRCLED DIGIT THREE] + fallthrough + case '\u2778': // ❸ [DINGBAT NEGATIVE CIRCLED DIGIT THREE] + fallthrough + case '\u2782': // ➂ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] + fallthrough + case '\u278C': // ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] + fallthrough + case '\uFF13': // 3 [FULLWIDTH DIGIT THREE] + output[outputPos] = '3' + outputPos++ + + case '\u248A': // ⒊ [DIGIT THREE FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '3' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2476': // ⑶ [PARENTHESIZED DIGIT THREE] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '3' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2074': // ⁴ [SUPERSCRIPT FOUR] + fallthrough + case '\u2084': // ₄ [SUBSCRIPT FOUR] + fallthrough + case '\u2463': // ④ [CIRCLED DIGIT FOUR] + fallthrough + case '\u24F8': // ⓸ [DOUBLE CIRCLED DIGIT FOUR] + fallthrough + case '\u2779': // ❹ [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] + fallthrough + case '\u2783': // ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] + fallthrough + case '\u278D': // ➍ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] + fallthrough + case '\uFF14': // 4 [FULLWIDTH DIGIT FOUR] + output[outputPos] = '4' + outputPos++ + + case '\u248B': // ⒋ [DIGIT FOUR FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '4' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2477': // ⑷ [PARENTHESIZED DIGIT FOUR] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '4' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2075': // ⁵ [SUPERSCRIPT FIVE] + fallthrough + case '\u2085': // ₅ [SUBSCRIPT FIVE] + fallthrough + case '\u2464': // ⑤ [CIRCLED DIGIT FIVE] + fallthrough + case '\u24F9': // ⓹ [DOUBLE CIRCLED DIGIT FIVE] + fallthrough + case '\u277A': // ❺ [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] + fallthrough + case '\u2784': // ➄ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] + fallthrough + case '\u278E': // ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] + fallthrough + case '\uFF15': // 5 [FULLWIDTH DIGIT FIVE] + output[outputPos] = '5' + outputPos++ + + case '\u248C': // ⒌ [DIGIT FIVE FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '5' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2478': // ⑸ [PARENTHESIZED DIGIT FIVE] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '5' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2076': // ⁶ [SUPERSCRIPT SIX] + fallthrough + case '\u2086': // ₆ [SUBSCRIPT SIX] + fallthrough + case '\u2465': // ⑥ [CIRCLED DIGIT SIX] + fallthrough + case '\u24FA': // ⓺ [DOUBLE CIRCLED DIGIT SIX] + fallthrough + case '\u277B': // ❻ [DINGBAT NEGATIVE CIRCLED DIGIT SIX] + fallthrough + case '\u2785': // ➅ [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] + fallthrough + case '\u278F': // ➏ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] + fallthrough + case '\uFF16': // 6 [FULLWIDTH DIGIT SIX] + output[outputPos] = '6' + outputPos++ + + case '\u248D': // ⒍ [DIGIT SIX FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '6' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2479': // ⑹ [PARENTHESIZED DIGIT SIX] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '6' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2077': // ⁷ [SUPERSCRIPT SEVEN] + fallthrough + case '\u2087': // ₇ [SUBSCRIPT SEVEN] + fallthrough + case '\u2466': // ⑦ [CIRCLED DIGIT SEVEN] + fallthrough + case '\u24FB': // ⓻ [DOUBLE CIRCLED DIGIT SEVEN] + fallthrough + case '\u277C': // ❼ [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] + fallthrough + case '\u2786': // ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] + fallthrough + case '\u2790': // ➐ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] + fallthrough + case '\uFF17': // 7 [FULLWIDTH DIGIT SEVEN] + output[outputPos] = '7' + outputPos++ + + case '\u248E': // ⒎ [DIGIT SEVEN FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '7' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u247A': // ⑺ [PARENTHESIZED DIGIT SEVEN] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '7' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2078': // ⁸ [SUPERSCRIPT EIGHT] + fallthrough + case '\u2088': // ₈ [SUBSCRIPT EIGHT] + fallthrough + case '\u2467': // ⑧ [CIRCLED DIGIT EIGHT] + fallthrough + case '\u24FC': // ⓼ [DOUBLE CIRCLED DIGIT EIGHT] + fallthrough + case '\u277D': // ❽ [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] + fallthrough + case '\u2787': // ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] + fallthrough + case '\u2791': // ➑ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] + fallthrough + case '\uFF18': // 8 [FULLWIDTH DIGIT EIGHT] + output[outputPos] = '8' + outputPos++ + + case '\u248F': // ⒏ [DIGIT EIGHT FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '8' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u247B': // ⑻ [PARENTHESIZED DIGIT EIGHT] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '8' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2079': // ⁹ [SUPERSCRIPT NINE] + fallthrough + case '\u2089': // ₉ [SUBSCRIPT NINE] + fallthrough + case '\u2468': // ⑨ [CIRCLED DIGIT NINE] + fallthrough + case '\u24FD': // ⓽ [DOUBLE CIRCLED DIGIT NINE] + fallthrough + case '\u277E': // ❾ [DINGBAT NEGATIVE CIRCLED DIGIT NINE] + fallthrough + case '\u2788': // ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] + fallthrough + case '\u2792': // ➒ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] + fallthrough + case '\uFF19': // 9 [FULLWIDTH DIGIT NINE] + output[outputPos] = '9' + outputPos++ + + case '\u2490': // ⒐ [DIGIT NINE FULL STOP] + output = output[:(len(output) + 1)] + output[outputPos] = '9' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u247C': // ⑼ [PARENTHESIZED DIGIT NINE] + output = output[:(len(output) + 2)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '9' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2469': // ⑩ [CIRCLED NUMBER TEN] + fallthrough + case '\u24FE': // ⓾ [DOUBLE CIRCLED NUMBER TEN] + fallthrough + case '\u277F': // ❿ [DINGBAT NEGATIVE CIRCLED NUMBER TEN] + fallthrough + case '\u2789': // ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] + fallthrough + case '\u2793': // ➓ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '0' + outputPos++ + + case '\u2491': // ⒑ [NUMBER TEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '0' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u247D': // ⑽ [PARENTHESIZED NUMBER TEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '0' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u246A': // ⑪ [CIRCLED NUMBER ELEVEN] + fallthrough + case '\u24EB': // ⓫ [NEGATIVE CIRCLED NUMBER ELEVEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '1' + outputPos++ + + case '\u2492': // ⒒ [NUMBER ELEVEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u247E': // ⑾ [PARENTHESIZED NUMBER ELEVEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u246B': // ⑫ [CIRCLED NUMBER TWELVE] + fallthrough + case '\u24EC': // ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '2' + outputPos++ + + case '\u2493': // ⒓ [NUMBER TWELVE FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '2' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u247F': // ⑿ [PARENTHESIZED NUMBER TWELVE] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '2' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u246C': // ⑬ [CIRCLED NUMBER THIRTEEN] + fallthrough + case '\u24ED': // ⓭ [NEGATIVE CIRCLED NUMBER THIRTEEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '3' + outputPos++ + + case '\u2494': // ⒔ [NUMBER THIRTEEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '3' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2480': // ⒀ [PARENTHESIZED NUMBER THIRTEEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '3' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u246D': // ⑭ [CIRCLED NUMBER FOURTEEN] + fallthrough + case '\u24EE': // ⓮ [NEGATIVE CIRCLED NUMBER FOURTEEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '4' + outputPos++ + + case '\u2495': // ⒕ [NUMBER FOURTEEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '4' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2481': // ⒁ [PARENTHESIZED NUMBER FOURTEEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '4' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u246E': // ⑮ [CIRCLED NUMBER FIFTEEN] + fallthrough + case '\u24EF': // ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '5' + outputPos++ + + case '\u2496': // ⒖ [NUMBER FIFTEEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '5' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2482': // ⒂ [PARENTHESIZED NUMBER FIFTEEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '5' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u246F': // ⑯ [CIRCLED NUMBER SIXTEEN] + fallthrough + case '\u24F0': // ⓰ [NEGATIVE CIRCLED NUMBER SIXTEEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '6' + outputPos++ + + case '\u2497': // ⒗ [NUMBER SIXTEEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '6' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2483': // ⒃ [PARENTHESIZED NUMBER SIXTEEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '6' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2470': // ⑰ [CIRCLED NUMBER SEVENTEEN] + fallthrough + case '\u24F1': // ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '7' + outputPos++ + + case '\u2498': // ⒘ [NUMBER SEVENTEEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '7' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2484': // ⒄ [PARENTHESIZED NUMBER SEVENTEEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '7' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2471': // ⑱ [CIRCLED NUMBER EIGHTEEN] + fallthrough + case '\u24F2': // ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '8' + outputPos++ + + case '\u2499': // ⒙ [NUMBER EIGHTEEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '8' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2485': // ⒅ [PARENTHESIZED NUMBER EIGHTEEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '8' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2472': // ⑲ [CIRCLED NUMBER NINETEEN] + fallthrough + case '\u24F3': // ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] + output = output[:(len(output) + 1)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '9' + outputPos++ + + case '\u249A': // ⒚ [NUMBER NINETEEN FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '1' + outputPos++ + output[outputPos] = '9' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2486': // ⒆ [PARENTHESIZED NUMBER NINETEEN] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '1' + outputPos++ + output[outputPos] = '9' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u2473': // ⑳ [CIRCLED NUMBER TWENTY] + fallthrough + case '\u24F4': // ⓴ [NEGATIVE CIRCLED NUMBER TWENTY] + output = output[:(len(output) + 1)] + output[outputPos] = '2' + outputPos++ + output[outputPos] = '0' + outputPos++ + + case '\u249B': // ⒛ [NUMBER TWENTY FULL STOP] + output = output[:(len(output) + 2)] + output[outputPos] = '2' + outputPos++ + output[outputPos] = '0' + outputPos++ + output[outputPos] = '.' + outputPos++ + + case '\u2487': // ⒇ [PARENTHESIZED NUMBER TWENTY] + output = output[:(len(output) + 3)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '2' + outputPos++ + output[outputPos] = '0' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u00AB': // « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] + fallthrough + case '\u00BB': // » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] + fallthrough + case '\u201C': // “ [LEFT DOUBLE QUOTATION MARK] + fallthrough + case '\u201D': // ” [RIGHT DOUBLE QUOTATION MARK] + fallthrough + case '\u201E': // „ [DOUBLE LOW-9 QUOTATION MARK] + fallthrough + case '\u2033': // ″ [DOUBLE PRIME] + fallthrough + case '\u2036': // ‶ [REVERSED DOUBLE PRIME] + fallthrough + case '\u275D': // ❝ [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] + fallthrough + case '\u275E': // ❞ [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] + fallthrough + case '\u276E': // ❮ [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] + fallthrough + case '\u276F': // ❯ [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] + fallthrough + case '\uFF02': // " [FULLWIDTH QUOTATION MARK] + output[outputPos] = '"' + outputPos++ + + case '\u2018': // ‘ [LEFT SINGLE QUOTATION MARK] + fallthrough + case '\u2019': // ’ [RIGHT SINGLE QUOTATION MARK] + fallthrough + case '\u201A': // ‚ [SINGLE LOW-9 QUOTATION MARK] + fallthrough + case '\u201B': // ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] + fallthrough + case '\u2032': // ′ [PRIME] + fallthrough + case '\u2035': // ‵ [REVERSED PRIME] + fallthrough + case '\u2039': // ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] + fallthrough + case '\u203A': // › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] + fallthrough + case '\u275B': // ❛ [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] + fallthrough + case '\u275C': // ❜ [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] + fallthrough + case '\uFF07': // ' [FULLWIDTH APOSTROPHE] + output[outputPos] = '\'' + outputPos++ + + case '\u2010': // ‐ [HYPHEN] + fallthrough + case '\u2011': // ‑ [NON-BREAKING HYPHEN] + fallthrough + case '\u2012': // ‒ [FIGURE DASH] + fallthrough + case '\u2013': // – [EN DASH] + fallthrough + case '\u2014': // — [EM DASH] + fallthrough + case '\u207B': // ⁻ [SUPERSCRIPT MINUS] + fallthrough + case '\u208B': // ₋ [SUBSCRIPT MINUS] + fallthrough + case '\uFF0D': // - [FULLWIDTH HYPHEN-MINUS] + output[outputPos] = '-' + outputPos++ + + case '\u2045': // ⁅ [LEFT SQUARE BRACKET WITH QUILL] + fallthrough + case '\u2772': // ❲ [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] + fallthrough + case '\uFF3B': // [ [FULLWIDTH LEFT SQUARE BRACKET] + output[outputPos] = '[' + outputPos++ + + case '\u2046': // ⁆ [RIGHT SQUARE BRACKET WITH QUILL] + fallthrough + case '\u2773': // ❳ [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] + fallthrough + case '\uFF3D': // ] [FULLWIDTH RIGHT SQUARE BRACKET] + output[outputPos] = ']' + outputPos++ + + case '\u207D': // ⁽ [SUPERSCRIPT LEFT PARENTHESIS] + fallthrough + case '\u208D': // ₍ [SUBSCRIPT LEFT PARENTHESIS] + fallthrough + case '\u2768': // ❨ [MEDIUM LEFT PARENTHESIS ORNAMENT] + fallthrough + case '\u276A': // ❪ [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] + fallthrough + case '\uFF08': // ( [FULLWIDTH LEFT PARENTHESIS] + output[outputPos] = '(' + outputPos++ + + case '\u2E28': // ⸨ [LEFT DOUBLE PARENTHESIS] + output = output[:(len(output) + 1)] + output[outputPos] = '(' + outputPos++ + output[outputPos] = '(' + outputPos++ + + case '\u207E': // ⁾ [SUPERSCRIPT RIGHT PARENTHESIS] + fallthrough + case '\u208E': // ₎ [SUBSCRIPT RIGHT PARENTHESIS] + fallthrough + case '\u2769': // ❩ [MEDIUM RIGHT PARENTHESIS ORNAMENT] + fallthrough + case '\u276B': // ❫ [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] + fallthrough + case '\uFF09': // ) [FULLWIDTH RIGHT PARENTHESIS] + output[outputPos] = ')' + outputPos++ + + case '\u2E29': // ⸩ [RIGHT DOUBLE PARENTHESIS] + output = output[:(len(output) + 1)] + output[outputPos] = ')' + outputPos++ + output[outputPos] = ')' + outputPos++ + + case '\u276C': // ❬ [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] + fallthrough + case '\u2770': // ❰ [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] + fallthrough + case '\uFF1C': // < [FULLWIDTH LESS-THAN SIGN] + output[outputPos] = '<' + outputPos++ + + case '\u276D': // ❭ [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] + fallthrough + case '\u2771': // ❱ [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] + fallthrough + case '\uFF1E': // > [FULLWIDTH GREATER-THAN SIGN] + output[outputPos] = '>' + outputPos++ + + case '\u2774': // ❴ [MEDIUM LEFT CURLY BRACKET ORNAMENT] + fallthrough + case '\uFF5B': // { [FULLWIDTH LEFT CURLY BRACKET] + output[outputPos] = '{' + outputPos++ + + case '\u2775': // ❵ [MEDIUM RIGHT CURLY BRACKET ORNAMENT] + fallthrough + case '\uFF5D': // } [FULLWIDTH RIGHT CURLY BRACKET] + output[outputPos] = '}' + outputPos++ + + case '\u207A': // ⁺ [SUPERSCRIPT PLUS SIGN] + fallthrough + case '\u208A': // ₊ [SUBSCRIPT PLUS SIGN] + fallthrough + case '\uFF0B': // + [FULLWIDTH PLUS SIGN] + output[outputPos] = '+' + outputPos++ + + case '\u207C': // ⁼ [SUPERSCRIPT EQUALS SIGN] + fallthrough + case '\u208C': // ₌ [SUBSCRIPT EQUALS SIGN] + fallthrough + case '\uFF1D': // = [FULLWIDTH EQUALS SIGN] + output[outputPos] = '=' + outputPos++ + + case '\uFF01': // ! [FULLWIDTH EXCLAMATION MARK] + output[outputPos] = '!' + outputPos++ + + case '\u203C': // ‼ [DOUBLE EXCLAMATION MARK] + output = output[:(len(output) + 1)] + output[outputPos] = '!' + outputPos++ + output[outputPos] = '!' + outputPos++ + + case '\u2049': // ⁉ [EXCLAMATION QUESTION MARK] + output = output[:(len(output) + 1)] + output[outputPos] = '!' + outputPos++ + output[outputPos] = '?' + outputPos++ + + case '\uFF03': // # [FULLWIDTH NUMBER SIGN] + output[outputPos] = '#' + outputPos++ + + case '\uFF04': // $ [FULLWIDTH DOLLAR SIGN] + output[outputPos] = '$' + outputPos++ + + case '\u2052': // ⁒ [COMMERCIAL MINUS SIGN] + fallthrough + case '\uFF05': // % [FULLWIDTH PERCENT SIGN] + output[outputPos] = '%' + outputPos++ + + case '\uFF06': // & [FULLWIDTH AMPERSAND] + output[outputPos] = '&' + outputPos++ + + case '\u204E': // ⁎ [LOW ASTERISK] + fallthrough + case '\uFF0A': // * [FULLWIDTH ASTERISK] + output[outputPos] = '*' + outputPos++ + + case '\uFF0C': // , [FULLWIDTH COMMA] + output[outputPos] = ',' + outputPos++ + + case '\uFF0E': // . [FULLWIDTH FULL STOP] + output[outputPos] = '.' + outputPos++ + + case '\u2044': // ⁄ [FRACTION SLASH] + fallthrough + case '\uFF0F': // / [FULLWIDTH SOLIDUS] + output[outputPos] = '/' + outputPos++ + + case '\uFF1A': // : [FULLWIDTH COLON] + output[outputPos] = ':' + outputPos++ + + case '\u204F': // ⁏ [REVERSED SEMICOLON] + fallthrough + case '\uFF1B': // ; [FULLWIDTH SEMICOLON] + output[outputPos] = ';' + outputPos++ + + case '\uFF1F': // ? [FULLWIDTH QUESTION MARK] + output[outputPos] = '?' + outputPos++ + + case '\u2047': // ⁇ [DOUBLE QUESTION MARK] + output = output[:(len(output) + 1)] + output[outputPos] = '?' + outputPos++ + output[outputPos] = '?' + outputPos++ + + case '\u2048': // ⁈ [QUESTION EXCLAMATION MARK] + output = output[:(len(output) + 1)] + output[outputPos] = '?' + outputPos++ + output[outputPos] = '!' + outputPos++ + + case '\uFF20': // @ [FULLWIDTH COMMERCIAL AT] + output[outputPos] = '@' + outputPos++ + + case '\uFF3C': // \ [FULLWIDTH REVERSE SOLIDUS] + output[outputPos] = '\\' + outputPos++ + + case '\u2038': // ‸ [CARET] + fallthrough + case '\uFF3E': // ^ [FULLWIDTH CIRCUMFLEX ACCENT] + output[outputPos] = '^' + outputPos++ + + case '\uFF3F': // _ [FULLWIDTH LOW LINE] + output[outputPos] = '_' + outputPos++ + + case '\u2053': // ⁓ [SWUNG DASH] + fallthrough + case '\uFF5E': // ~ [FULLWIDTH TILDE] + output[outputPos] = '~' + outputPos++ + + default: + output[outputPos] = c + outputPos++ + } + } + } + return output +} diff --git a/analysis/char/asciifolding/asciifolding_test.go b/analysis/char/asciifolding/asciifolding_test.go new file mode 100644 index 0000000..52f1594 --- /dev/null +++ b/analysis/char/asciifolding/asciifolding_test.go @@ -0,0 +1,124 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package asciifolding + +import ( + "fmt" + "reflect" + "testing" +) + +func TestAsciiFoldingFilter(t *testing.T) { + tests := []struct { + input []byte + output []byte + }{ + { + // empty input passes + input: []byte(``), + output: []byte(``), + }, + { + // no modification for plain ASCII + input: []byte(`The quick brown fox jumps over the lazy dog`), + output: []byte(`The quick brown fox jumps over the lazy dog`), + }, + { + // Umlauts are folded to plain ASCII + input: []byte(`The quick bröwn fox jümps over the läzy dog`), + output: []byte(`The quick brown fox jumps over the lazy dog`), + }, + { + // composite unicode runes are folded to more than one ASCII rune + input: []byte(`ÆꜴ`), + output: []byte(`AEAO`), + }, + { + // apples from https://issues.couchbase.com/browse/MB-33486 + input: []byte(`Ápple Àpple Äpple Âpple Ãpple Åpple`), + output: []byte(`Apple Apple Apple Apple Apple Apple`), + }, + { + // Fix ASCII folding of \u24A2 + input: []byte(`⒢`), + output: []byte(`(g)`), + }, + { + // Test folding of \u2053 (SWUNG DASH) + input: []byte(`a⁓b`), + output: []byte(`a~b`), + }, + { + // Test folding of \uFF5E (FULLWIDTH TILDE) + input: []byte(`c~d`), + output: []byte(`c~d`), + }, + { + // Test folding of \uFF3F (FULLWIDTH LOW LINE) - case before tilde + input: []byte(`e_f`), + output: []byte(`e_f`), + }, + { + // Test mix including tilde and default fallthrough (using a character not explicitly folded) + input: []byte(`a⁓b✅c~d`), + output: []byte(`a~b✅c~d`), + }, + { + // Test start of 'A' fallthrough block + input: []byte(`ÀBC`), + output: []byte(`ABC`), + }, + { + // Test end of 'A' fallthrough block + input: []byte(`DEFẶ`), + output: []byte(`DEFA`), + }, + { + // Test start of 'AE' fallthrough block + input: []byte(`Æ`), + output: []byte(`AE`), + }, + { + // Test end of 'AE' fallthrough block + input: []byte(`ᴁ`), + output: []byte(`AE`), + }, + { + // Test 'DZ' multi-rune output + input: []byte(`DŽebra`), + output: []byte(`DZebra`), + }, + { + // Test start of 'a' fallthrough block + input: []byte(`àbc`), + output: []byte(`abc`), + }, + { + // Test end of 'a' fallthrough block + input: []byte(`defa`), + output: []byte(`defa`), + }, + } + + for _, test := range tests { + filter := New() + t.Run(fmt.Sprintf("on %s", test.input), func(t *testing.T) { + output := filter.Filter(test.input) + if !reflect.DeepEqual(output, test.output) { + t.Errorf("\nExpected:\n`%s`\ngot:\n`%s`\n", string(test.output), string(output)) + } + }) + } +} diff --git a/analysis/char/html/html.go b/analysis/char/html/html.go new file mode 100644 index 0000000..d939fdb --- /dev/null +++ b/analysis/char/html/html.go @@ -0,0 +1,57 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package html + +import ( + "bytes" + "regexp" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "html" + +var htmlCharFilterRegexp = regexp.MustCompile(`\s]+))?)+\s*|\s*)/?>`) + +type CharFilter struct { + r *regexp.Regexp + replacement []byte +} + +func New() *CharFilter { + return &CharFilter{ + r: htmlCharFilterRegexp, + replacement: []byte(" "), + } +} + +func (s *CharFilter) Filter(input []byte) []byte { + return s.r.ReplaceAllFunc( + input, func(in []byte) []byte { + return bytes.Repeat(s.replacement, len(in)) + }) +} + +func CharFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.CharFilter, error) { + return New(), nil +} + +func init() { + err := registry.RegisterCharFilter(Name, CharFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/char/regexp/regexp.go b/analysis/char/regexp/regexp.go new file mode 100644 index 0000000..a94236a --- /dev/null +++ b/analysis/char/regexp/regexp.go @@ -0,0 +1,65 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package regexp + +import ( + "fmt" + "regexp" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "regexp" + +type CharFilter struct { + r *regexp.Regexp + replacement []byte +} + +func New(r *regexp.Regexp, replacement []byte) *CharFilter { + return &CharFilter{ + r: r, + replacement: replacement, + } +} + +func (s *CharFilter) Filter(input []byte) []byte { + return s.r.ReplaceAll(input, s.replacement) +} + +func CharFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.CharFilter, error) { + regexpStr, ok := config["regexp"].(string) + if !ok { + return nil, fmt.Errorf("must specify regexp") + } + r, err := regexp.Compile(regexpStr) + if err != nil { + return nil, fmt.Errorf("unable to build regexp char filter: %v", err) + } + replaceBytes := []byte(" ") + replaceStr, ok := config["replace"].(string) + if ok { + replaceBytes = []byte(replaceStr) + } + return New(r, replaceBytes), nil +} + +func init() { + err := registry.RegisterCharFilter(Name, CharFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/char/regexp/regexp_test.go b/analysis/char/regexp/regexp_test.go new file mode 100644 index 0000000..ff1b04b --- /dev/null +++ b/analysis/char/regexp/regexp_test.go @@ -0,0 +1,88 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package regexp + +import ( + "fmt" + "reflect" + "regexp" + "testing" +) + +func TestRegexpCharFilter(t *testing.T) { + + tests := []struct { + regexStr string + replace []byte + input []byte + output []byte + }{ + { + regexStr: `\s]+))?)+\s*|\s*)/?>`, + replace: []byte{' '}, + input: []byte(`test`), + output: []byte(` test `), + }, + { + regexStr: `\x{200C}`, + replace: []byte{' '}, + input: []byte("water\u200Cunder\u200Cthe\u200Cbridge"), + output: []byte("water under the bridge"), + }, + { + regexStr: `([a-z])\s+(\d)`, + replace: []byte(`$1-$2`), + input: []byte(`temp 1`), + output: []byte(`temp-1`), + }, + { + regexStr: `foo.?`, + replace: []byte(`X`), + input: []byte(`seafood, fool`), + output: []byte(`seaX, X`), + }, + { + regexStr: `def`, + replace: []byte(`_`), + input: []byte(`abcdefghi`), + output: []byte(`abc_ghi`), + }, + { + regexStr: `456`, + replace: []byte(`000000`), + input: []byte(`123456789`), + output: []byte(`123000000789`), + }, + { + regexStr: `“|”`, + replace: []byte(`"`), + input: []byte(`“hello”`), + output: []byte(`"hello"`), + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("match %s replace %s", test.regexStr, string(test.replace)), func(t *testing.T) { + regex := regexp.MustCompile(test.regexStr) + filter := New(regex, test.replace) + + output := filter.Filter(test.input) + if !reflect.DeepEqual(test.output, output) { + t.Errorf("Expected: `%s`, Got: `%s`\n", string(test.output), string(output)) + } + }) + + } +} diff --git a/analysis/char/zerowidthnonjoiner/zerowidthnonjoiner.go b/analysis/char/zerowidthnonjoiner/zerowidthnonjoiner.go new file mode 100644 index 0000000..0a39fa6 --- /dev/null +++ b/analysis/char/zerowidthnonjoiner/zerowidthnonjoiner.go @@ -0,0 +1,39 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zerowidthnonjoiner + +import ( + "regexp" + + "github.com/blevesearch/bleve/v2/analysis" + regexpCharFilter "github.com/blevesearch/bleve/v2/analysis/char/regexp" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "zero_width_spaces" + +var zeroWidthNonJoinerRegexp = regexp.MustCompile(`\x{200C}`) + +func CharFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.CharFilter, error) { + replaceBytes := []byte(" ") + return regexpCharFilter.New(zeroWidthNonJoinerRegexp, replaceBytes), nil +} + +func init() { + err := registry.RegisterCharFilter(Name, CharFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/flexible/flexible.go b/analysis/datetime/flexible/flexible.go new file mode 100644 index 0000000..36cc9e8 --- /dev/null +++ b/analysis/datetime/flexible/flexible.go @@ -0,0 +1,67 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flexible + +import ( + "fmt" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "flexiblego" + +type DateTimeParser struct { + layouts []string +} + +func New(layouts []string) *DateTimeParser { + return &DateTimeParser{ + layouts: layouts, + } +} + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + for _, layout := range p.layouts { + rv, err := time.Parse(layout, input) + if err == nil { + return rv, layout, nil + } + } + return time.Time{}, "", analysis.ErrInvalidDateTime +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + layouts, ok := config["layouts"].([]interface{}) + if !ok { + return nil, fmt.Errorf("must specify layouts") + } + var layoutStrs []string + for _, layout := range layouts { + layoutStr, ok := layout.(string) + if ok { + layoutStrs = append(layoutStrs, layoutStr) + } + } + return New(layoutStrs), nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/flexible/flexible_test.go b/analysis/datetime/flexible/flexible_test.go new file mode 100644 index 0000000..12aedb2 --- /dev/null +++ b/analysis/datetime/flexible/flexible_test.go @@ -0,0 +1,100 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flexible + +import ( + "reflect" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestFlexibleDateTimeParser(t *testing.T) { + testLocation := time.FixedZone("", -8*60*60) + + rfc3339NoTimezone := "2006-01-02T15:04:05" + rfc3339NoTimezoneNoT := "2006-01-02 15:04:05" + rfc3339NoTime := "2006-01-02" + + dateOptionalTimeParser := New( + []string{ + time.RFC3339Nano, + time.RFC3339, + rfc3339NoTimezone, + rfc3339NoTimezoneNoT, + rfc3339NoTime, + }) + + tests := []struct { + input string + expectedTime time.Time + expectedLayout string + expectedError error + }{ + { + input: "2014-08-03", + expectedTime: time.Date(2014, 8, 3, 0, 0, 0, 0, time.UTC), + expectedLayout: rfc3339NoTime, + expectedError: nil, + }, + { + input: "2014-08-03T15:59:30", + expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 0, time.UTC), + expectedLayout: rfc3339NoTimezone, + expectedError: nil, + }, + { + input: "2014-08-03 15:59:30", + expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 0, time.UTC), + expectedLayout: rfc3339NoTimezoneNoT, + expectedError: nil, + }, + { + input: "2014-08-03T15:59:30-08:00", + expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 0, testLocation), + expectedLayout: time.RFC3339Nano, + expectedError: nil, + }, + { + + input: "2014-08-03T15:59:30.999999999-08:00", + expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 999999999, testLocation), + expectedLayout: time.RFC3339Nano, + expectedError: nil, + }, + { + input: "not a date time", + expectedTime: time.Time{}, + expectedLayout: "", + expectedError: analysis.ErrInvalidDateTime, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + actualTime, actualLayout, actualErr := dateOptionalTimeParser.ParseDateTime(test.input) + if actualErr != test.expectedError { + t.Fatalf("expected error %#v, got %#v", test.expectedError, actualErr) + } + if !reflect.DeepEqual(actualTime, test.expectedTime) { + t.Errorf("expected time %v, got %v", test.expectedTime, actualTime) + } + if !reflect.DeepEqual(actualLayout, test.expectedLayout) { + t.Errorf("expected layout %v, got %v", test.expectedLayout, actualLayout) + } + }) + } +} diff --git a/analysis/datetime/iso/iso.go b/analysis/datetime/iso/iso.go new file mode 100644 index 0000000..df947a6 --- /dev/null +++ b/analysis/datetime/iso/iso.go @@ -0,0 +1,250 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iso + +import ( + "fmt" + "strings" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "isostyle" + +var textLiteralDelimiter byte = '\'' // single quote + +// ISO style date strings are represented in +// https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html +// +// Some format specifiers are not specified in go time package, such as: +// - 'V' for timezone name, like 'Europe/Berlin' or 'America/New_York'. +// - 'Q' for quarter of year, like Q3 or 3rd Quarter. +// - 'zzzz' for full name of timezone like "Japan Standard Time" or "Eastern Standard Time". +// - 'O' for localized zone-offset, like GMT+8 or GMT+08:00. +// - '[]' for optional section of the format. +// - 'G' for era, like AD or BC. +// - 'W' for week of month. +// - 'D' for day of year. +// So date strings with these date elements cannot be parsed. +var timeElementToLayout = map[byte]map[int]string{ + 'M': { + 4: "January", // MMMM = full month name + 3: "Jan", // MMM = short month name + 2: "01", // MM = month of year (2 digits) (01-12) + 1: "1", // M = month of year (1 digit) (1-12) + }, + 'd': { + 2: "02", // dd = day of month (2 digits) (01-31) + 1: "2", // d = day of month (1 digit) (1-31) + }, + 'a': { + 2: "pm", // aa = pm/am + 1: "PM", // a = PM/AM + }, + 'H': { + 2: "15", // HH = hour (24 hour clock) (2 digits) + 1: "15", // H = hour (24 hour clock) (1 digit) + }, + 'm': { + 2: "04", // mm = minute (2 digits) + 1: "4", // m = minute (1 digit) + }, + 's': { + 2: "05", // ss = seconds (2 digits) + 1: "5", // s = seconds (1 digit) + }, + + // timezone offsets from UTC below + 'X': { + 5: "Z07:00:00", // XXXXX = timezone offset (+-hh:mm:ss) + 4: "Z070000", // XXXX = timezone offset (+-hhmmss) + 3: "Z07:00", // XXX = timezone offset (+-hh:mm) + 2: "Z0700", // XX = timezone offset (+-hhmm) + 1: "Z07", // X = timezone offset (+-hh) + }, + 'x': { + 5: "-07:00:00", // xxxxx = timezone offset (+-hh:mm:ss) + 4: "-070000", // xxxx = timezone offset (+-hhmmss) + 3: "-07:00", // xxx = timezone offset (+-hh:mm) + 2: "-0700", // xx = timezone offset (+-hhmm) + 1: "-07", // x = timezone offset (+-hh) + }, +} + +type DateTimeParser struct { + layouts []string +} + +func New(layouts []string) *DateTimeParser { + return &DateTimeParser{ + layouts: layouts, + } +} + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + for _, layout := range p.layouts { + rv, err := time.Parse(layout, input) + if err == nil { + return rv, layout, nil + } + } + return time.Time{}, "", analysis.ErrInvalidDateTime +} + +func letterCounter(layout string, idx int) int { + count := 1 + for idx+count < len(layout) { + if layout[idx+count] == layout[idx] { + count++ + } else { + break + } + } + return count +} + +func invalidFormatError(character byte, count int) error { + return fmt.Errorf("invalid format string, unknown format specifier: " + strings.Repeat(string(character), count)) +} + +func parseISOString(layout string) (string, error) { + var dateTimeLayout strings.Builder + + for idx := 0; idx < len(layout); { + // check if the character is a text literal delimiter (') + if layout[idx] == textLiteralDelimiter { + if idx+1 < len(layout) && layout[idx+1] == textLiteralDelimiter { + // if the next character is also a text literal delimiter, then + // copy the character as is + dateTimeLayout.WriteByte(textLiteralDelimiter) + idx += 2 + continue + } + // find the next text literal delimiter + for idx++; idx < len(layout); idx++ { + if layout[idx] == textLiteralDelimiter { + break + } + dateTimeLayout.WriteByte(layout[idx]) + } + // idx can either be equal to len(layout) if the text literal delimiter is not found + // after the first text literal delimiter or it will be equal to the index of the + // second text literal delimiter + if idx == len(layout) { + // text literal delimiter not found error + return "", fmt.Errorf("invalid format string, expected text literal delimiter: " + string(textLiteralDelimiter)) + } + // increment idx to skip the second text literal delimiter + idx++ + continue + } + // check if character is a letter in english alphabet - a-zA-Z which are reserved + // for format specifiers + if (layout[idx] >= 'a' && layout[idx] <= 'z') || (layout[idx] >= 'A' && layout[idx] <= 'Z') { + // find the number of times the character occurs consecutively + count := letterCounter(layout, idx) + character := layout[idx] + // first check the table + if layout, ok := timeElementToLayout[character][count]; ok { + dateTimeLayout.WriteString(layout) + } else { + switch character { + case 'y', 'u', 'Y': + // year + if count == 2 { + dateTimeLayout.WriteString("06") + } else { + format := fmt.Sprintf("%%0%ds", count) + dateTimeLayout.WriteString(fmt.Sprintf(format, "2006")) + } + case 'h', 'K': + // hour (1-12) + switch count { + case 2: + // hh, KK -> 03 + dateTimeLayout.WriteString("03") + case 1: + // h, K -> 3 + dateTimeLayout.WriteString("3") + default: + // e.g., hhh + return "", invalidFormatError(character, count) + } + case 'E': + // day of week + if count == 4 { + dateTimeLayout.WriteString("Monday") // EEEE -> Monday + } else if count <= 3 { + dateTimeLayout.WriteString("Mon") // E, EE, EEE -> Mon + } else { + return "", invalidFormatError(character, count) // e.g., EEEEE + } + case 'S': + // fraction of second + // .SSS = millisecond + // .SSSSSS = microsecond + // .SSSSSSSSS = nanosecond + if count > 9 { + return "", invalidFormatError(character, count) + } + dateTimeLayout.WriteString(strings.Repeat(string('0'), count)) + case 'z': + // timezone id + if count < 5 { + dateTimeLayout.WriteString("MST") + } else { + return "", invalidFormatError(character, count) + } + default: + return "", invalidFormatError(character, count) + } + } + idx += count + } else { + // copy the character as is + dateTimeLayout.WriteByte(layout[idx]) + idx++ + } + } + return dateTimeLayout.String(), nil +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + layouts, ok := config["layouts"].([]interface{}) + if !ok { + return nil, fmt.Errorf("must specify layouts") + } + var layoutStrs []string + for _, layout := range layouts { + layoutStr, ok := layout.(string) + if ok { + layout, err := parseISOString(layoutStr) + if err != nil { + return nil, err + } + layoutStrs = append(layoutStrs, layout) + } + } + return New(layoutStrs), nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/iso/iso_test.go b/analysis/datetime/iso/iso_test.go new file mode 100644 index 0000000..742e96b --- /dev/null +++ b/analysis/datetime/iso/iso_test.go @@ -0,0 +1,182 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iso + +import ( + "fmt" + "testing" +) + +func TestConversionFromISOStyle(t *testing.T) { + tests := []struct { + input string + output string + err error + }{ + { + input: "yyyy-MM-dd", + output: "2006-01-02", + err: nil, + }, + { + input: "uuu/M''''dd'T'HH:m:ss.SSS", + output: "2006/1''02T15:4:05.000", + err: nil, + }, + { + input: "YYYY-MM-dd'T'H:mm:ss zzz", + output: "2006-01-02T15:04:05 MST", + err: nil, + }, + { + input: "MMMM dd yyyy', 'HH:mm:ss.SSS", + output: "January 02 2006, 15:04:05.000", + err: nil, + }, + { + input: "h 'o'''' clock' a, XXX", + output: "3 o' clock PM, Z07:00", + err: nil, + }, + { + input: "YYYY-MM-dd'T'HH:mm:ss'Z'", + output: "2006-01-02T15:04:05Z", + err: nil, + }, + { + input: "E MMM d H:mm:ss z Y", + output: "Mon Jan 2 15:04:05 MST 2006", + err: nil, + }, + { + input: "E MMM DD H:m:s z Y", + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: DD"), + }, + { + input: "E MMM''''' H:m:s z Y", + output: "", + err: fmt.Errorf("invalid format string, expected text literal delimiter: '"), + }, + { + input: "MMMMM dd yyyy', 'HH:mm:ss.SSS", + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: MMMMM"), + }, + { + input: "yy", // year (2 digits) + output: "06", + err: nil, + }, + { + input: "yyyyy", // year (5 digits, padded) + output: "02006", + err: nil, + }, + { + input: "h", // hour 1-12 (1 digit) + output: "3", + err: nil, + }, + { + input: "hh", // hour 1-12 (2 digits) + output: "03", + err: nil, + }, + { + input: "KK", // hour 1-12 (2 digits, alt) + output: "03", + err: nil, + }, + { + input: "hhh", // invalid hour count + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: hhh"), + }, + { + input: "E", // Day of week (short) + output: "Mon", + err: nil, + }, + { + input: "EEE", // Day of week (short) + output: "Mon", + err: nil, + }, + { + input: "EEEE", // Day of week (long) + output: "Monday", + err: nil, + }, + { + input: "EEEEE", // Day of week (long) + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: EEEEE"), + }, + { + input: "S", // Fraction of second (1 digit) + output: "0", + err: nil, + }, + { + input: "SSSSSSSSS", // Fraction of second (9 digits) + output: "000000000", + err: nil, + }, + { + input: "SSSSSSSSSS", // Invalid fraction of second count + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: SSSSSSSSSS"), + }, + { + input: "z", // Timezone name (short) + output: "MST", + err: nil, + }, + { + input: "zzz", // Timezone name (short) - Corrected expectation + output: "MST", // Should output MST + err: nil, // Should not produce an error + }, + { + input: "zzzz", // Timezone name (long) - Corrected expectation + output: "MST", // Should output MST + err: nil, // Should not produce an error + }, + { + input: "G", // Era designator (unsupported) + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: G"), + }, + { + input: "W", // Week of month (unsupported) + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: W"), + }, + } + for i, test := range tests { + t.Run(fmt.Sprintf("test %d: %s", i, test.input), func(t *testing.T) { + out, err := parseISOString(test.input) + // Check error matching + if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) { + t.Fatalf("expected error %v, got error %v", test.err, err) + } + // Check output matching only if no error was expected/occurred + if err == nil && test.err == nil && out != test.output { + t.Fatalf("expected output '%v', got '%v'", test.output, out) + } + }) + } +} diff --git a/analysis/datetime/optional/optional.go b/analysis/datetime/optional/optional.go new file mode 100644 index 0000000..db30049 --- /dev/null +++ b/analysis/datetime/optional/optional.go @@ -0,0 +1,50 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package optional + +import ( + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/datetime/flexible" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "dateTimeOptional" + +const rfc3339NoTimezone = "2006-01-02T15:04:05" +const rfc3339NoTimezoneNoT = "2006-01-02 15:04:05" +const rfc3339Offset = "2006-01-02 15:04:05 -0700" +const rfc3339NoTime = "2006-01-02" + +var layouts = []string{ + time.RFC3339Nano, + time.RFC3339, + rfc3339NoTimezone, + rfc3339NoTimezoneNoT, + rfc3339Offset, + rfc3339NoTime, +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + return flexible.New(layouts), nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/percent/percent.go b/analysis/datetime/percent/percent.go new file mode 100644 index 0000000..6526aae --- /dev/null +++ b/analysis/datetime/percent/percent.go @@ -0,0 +1,205 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package percent + +import ( + "fmt" + "strings" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "percentstyle" + +var formatDelimiter byte = '%' + +// format specifiers as per strftime in the C standard library +// https://man7.org/linux/man-pages/man3/strftime.3.html +var formatSpecifierToLayout = map[byte]string{ + formatDelimiter: string(formatDelimiter), // %% = % (literal %) + 'a': "Mon", // %a = short weekday name + 'A': "Monday", // %A = full weekday name + 'd': "02", // %d = day of month (2 digits) (01-31) + 'e': "2", // %e = day of month (1 digit) (1-31) + 'b': "Jan", // %b = short month name + 'B': "January", // %B = full month name + 'm': "01", // %m = month of year (2 digits) (01-12) + 'y': "06", // %y = year without century + 'Y': "2006", // %Y = year with century + 'H': "15", // %H = hour (24 hour clock) (2 digits) + 'I': "03", // %I = hour (12 hour clock) (2 digits) + 'l': "3", // %l = hour (12 hour clock) (1 digit) + 'p': "PM", // %p = PM/AM + 'P': "pm", // %P = pm/am (lowercase) + 'M': "04", // %M = minute (2 digits) + 'S': "05", // %S = seconds (2 digits) + 'f': "999999", // .%f = fraction of seconds - up to microseconds (6 digits) - deci/milli/micro + 'Z': "MST", // %Z = timezone name (GMT, JST, UTC etc) + // %z is present in timezone options + + // some additional options not in strftime to support additional options such as + // disallow 0 padding in minute and seconds, nanosecond precision, etc + 'o': "1", // %o = month of year (1 digit) (1-12) + 'i': "4", // %i = minute (1 digit) + 's': "5", // %s = seconds (1 digit) + 'N': "999999999", // .%N = fraction of seconds - up to microseconds (9 digits) - milli/micro/nano +} + +// some additional options for timezone +// such as allowing colon in timezone offset and specifying the seconds +// timezone offsets are from UTC +var timezoneOptions = map[string]string{ + "z": "Z0700", // %z = timezone offset in +-hhmm / +-(2 digit hour)(2 digit minute) +0500, -0600 etc + "z:M": "Z07:00", // %z:M = timezone offset(+-hh:mm) / +-(2 digit hour):(2 digit minute) +05:00, -06:00 etc + "z:S": "Z07:00:00", // %z:M = timezone offset(+-hh:mm:ss) / +-(2 digit hour):(2 digit minute):(2 digit second) +05:20:00, -06:30:00 etc + "zH": "Z07", // %zH = timezone offset(+-hh) / +-(2 digit hour) +05, -06 etc + "zS": "Z070000", // %zS = timezone offset(+-hhmmss) / +-(2 digit hour)(2 digit minute)(2 digit second) +052000, -063000 etc +} + +type DateTimeParser struct { + layouts []string +} + +func New(layouts []string) *DateTimeParser { + return &DateTimeParser{ + layouts: layouts, + } +} + +func checkTZOptions(formatString string, idx int) (string, int) { + // idx points to '%' + // We know formatString[idx+1] == 'z' + nextIdx := idx + 2 // Index of the character immediately after 'z' + + // Default values assume only '%z' is present + layout := timezoneOptions["z"] + finalIdx := nextIdx // Index after '%z' + + if nextIdx < len(formatString) { + switch formatString[nextIdx] { + case ':': + // Check for modifier after the colon ':' + colonModifierIdx := nextIdx + 1 + if colonModifierIdx < len(formatString) { + switch formatString[colonModifierIdx] { + case 'M': + // Found %z:M + layout = timezoneOptions["z:M"] + finalIdx = colonModifierIdx + 1 // Index after %z:M + case 'S': + // Found %z:S + layout = timezoneOptions["z:S"] + finalIdx = colonModifierIdx + 1 // Index after %z:S + // default: If %z: is followed by something else, or just %z: at the end. + // Keep the default layout ("z") and finalIdx (idx + 2). + // The ':' will be treated as a literal by the main loop. + } + } + // else: %z: is at the very end of the string. + // Keep the default layout ("z") and finalIdx (idx + 2). + // The ':' will be treated as a literal by the main loop. + + case 'H': + // Found %zH + layout = timezoneOptions["zH"] + finalIdx = nextIdx + 1 // Index after %zH + case 'S': + // Found %zS + layout = timezoneOptions["zS"] + finalIdx = nextIdx + 1 // Index after %zS + + // default: If %z is followed by something other than ':', 'H', or 'S'. + // Keep the default layout ("z") and finalIdx (idx + 2). + // The character formatString[nextIdx] will be handled by the main loop. + } + } + // else: %z is at the very end of the string. + // Keep the default layout ("z") and finalIdx (idx + 2). + + return layout, finalIdx +} + +func parseFormatString(formatString string) (string, error) { + var dateTimeLayout strings.Builder + // iterate over the format string and replace the format specifiers with + // the corresponding golang constants + for idx := 0; idx < len(formatString); { + // check if the character is a format delimiter (%) + if formatString[idx] == formatDelimiter { + // check if there is a character after the format delimiter (%) + if idx+1 >= len(formatString) { + return "", fmt.Errorf("invalid format string, expected character after %s", string(formatDelimiter)) + } + formatSpecifier := formatString[idx+1] + if layout, ok := formatSpecifierToLayout[formatSpecifier]; ok { + dateTimeLayout.WriteString(layout) + idx += 2 + } else if formatSpecifier == 'z' { + // did not find a valid specifier + // check if it is for timezone + var tzLayout string + tzLayout, idx = checkTZOptions(formatString, idx) + dateTimeLayout.WriteString(tzLayout) + } else { + return "", fmt.Errorf("invalid format string, unknown format specifier: %s", string(formatSpecifier)) + } + continue + } + // copy the character as is + dateTimeLayout.WriteByte(formatString[idx]) + idx++ + } + return dateTimeLayout.String(), nil +} + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + for _, layout := range p.layouts { + rv, err := time.Parse(layout, input) + if err == nil { + return rv, layout, nil + } + } + return time.Time{}, "", analysis.ErrInvalidDateTime +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + layouts, ok := config["layouts"].([]interface{}) + if !ok { + return nil, fmt.Errorf("must specify layouts") + } + + layoutStrs := make([]string, 0, len(layouts)) + for _, layout := range layouts { + layoutStr, ok := layout.(string) + if ok { + layout, err := parseFormatString(layoutStr) + if err != nil { + return nil, err + } + layoutStrs = append(layoutStrs, layout) + } + } + + return New(layoutStrs), nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/percent/percent_test.go b/analysis/datetime/percent/percent_test.go new file mode 100644 index 0000000..70d0970 --- /dev/null +++ b/analysis/datetime/percent/percent_test.go @@ -0,0 +1,474 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package percent + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestConversionFromPercentStyle(t *testing.T) { + tests := []struct { + name string // Added name field + input string + output string + err error + }{ + { + name: "basic YMD", + input: "%Y-%m-%d", + output: "2006-01-02", + err: nil, + }, + { + name: "YMD with double percent and literal T", + input: "%Y/%m%%%%%dT%H%M:%S", + output: "2006/01%%02T1504:05", + err: nil, + }, + { + name: "YMD T HMS Z z", + input: "%Y-%m-%dT%H:%M:%S %Z%z", + output: "2006-01-02T15:04:05 MSTZ0700", + err: nil, + }, + { + name: "Full month, padded day/hour, am/pm, z:M", + input: "%B %e, %Y %l:%i %P %z:M", + output: "January 2, 2006 3:4 pm Z07:00", + err: nil, + }, + { + name: "Long format with literals and timezone literal :S", + input: "Hour %H Minute %Mseconds %S.%N Timezone:%Z:S, Weekday %a; Day %d Month %b, Year %y", + output: "Hour 15 Minute 04seconds 05.999999999 Timezone:MST:S, Weekday Mon; Day 02 Month Jan, Year 06", + err: nil, + }, + { + name: "YMD T HMS with nanoseconds", + input: "%Y-%m-%dT%H:%M:%S.%N", + output: "2006-01-02T15:04:05.999999999", + err: nil, + }, + { + name: "HMS Z z", + input: "%H:%M:%S %Z %z", + output: "15:04:05 MST Z0700", + err: nil, + }, + { + name: "HMS Z z literal colon", + input: "%H:%M:%S %Z %z:", + output: "15:04:05 MST Z0700:", + err: nil, + }, + { + name: "HMS Z z:M", + input: "%H:%M:%S %Z %z:M", + output: "15:04:05 MST Z07:00", + err: nil, + }, + { + name: "HMS Z z:S", + input: "%H:%M:%S %Z %z:S", + output: "15:04:05 MST Z07:00:00", + err: nil, + }, + { + name: "HMS Z z: literal A", + input: "%H:%M:%S %Z %z:A", + output: "15:04:05 MST Z0700:A", + err: nil, + }, + { + name: "HMS Z z literal M", + input: "%H:%M:%S %Z %zM", + output: "15:04:05 MST Z0700M", + err: nil, + }, + { + name: "HMS Z zH", + input: "%H:%M:%S %Z %zH", + output: "15:04:05 MST Z07", + err: nil, + }, + { + name: "HMS Z zS", + input: "%H:%M:%S %Z %zS", + output: "15:04:05 MST Z070000", + err: nil, + }, + { + name: "Complex combination z zS z: zH", + input: "%H:%M:%S %Z %z%Z %zS%z:%zH", + output: "15:04:05 MST Z0700MST Z070000Z0700:Z07", + err: nil, + }, + { + name: "z at end", + input: "%Y-%m-%d %z", + output: "2006-01-02 Z0700", + err: nil, + }, + { + name: "z: at end", + input: "%Y-%m-%d %z:", + output: "2006-01-02 Z0700:", + err: nil, + }, + { + name: "zH at end", + input: "%Y-%m-%d %zH", + output: "2006-01-02 Z07", + err: nil, + }, + { + name: "zS at end", + input: "%Y-%m-%d %zS", + output: "2006-01-02 Z070000", + err: nil, + }, + { + name: "z:M at end", + input: "%Y-%m-%d %z:M", + output: "2006-01-02 Z07:00", + err: nil, + }, + { + name: "z:S at end", + input: "%Y-%m-%d %z:S", + output: "2006-01-02 Z07:00:00", + err: nil, + }, + { + name: "z followed by literal X", + input: "%Y-%m-%d %zX", + output: "2006-01-02 Z0700X", + err: nil, + }, + { + name: "z: followed by literal X", + input: "%Y-%m-%d %z:X", + output: "2006-01-02 Z0700:X", + err: nil, + }, + { + name: "Invalid specifier T", + input: "%Y-%m-%d%T%H:%M:%S %ZM", + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: T"), + }, + { + name: "Ends with %", + input: "%Y-%m-%dT%H:%M:%S %ZM%", + output: "", + err: fmt.Errorf("invalid format string, expected character after %%"), + }, + { + name: "Just %", + input: "%", + output: "", + err: fmt.Errorf("invalid format string, expected character after %%"), + }, + { + name: "Just %%", + input: "%%", + output: "%", + err: nil, + }, + { + name: "Unknown specifier x", + input: "%x", + output: "", + err: fmt.Errorf("invalid format string, unknown format specifier: x"), + }, + { + name: "Literal prefix", + input: "literal %Y", + output: "literal 2006", + err: nil, + }, + { + name: "Literal suffix", + input: "%Y literal", + output: "2006 literal", + err: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + out, err := parseFormatString(test.input) + + // Enhanced Error Check: + expectedErrStr := "" + if test.err != nil { + expectedErrStr = test.err.Error() + } + actualErrStr := "" + if err != nil { + actualErrStr = err.Error() + } + + if expectedErrStr != actualErrStr { + // Provide more detailed output if errors don't match as strings + t.Fatalf("error mismatch:\nExpected error: %q\nGot error : %q", expectedErrStr, actualErrStr) + } + + // Original error presence check (redundant if string check passes, but safe to keep) + if (err != nil && test.err == nil) || (err == nil && test.err != nil) { + t.Fatalf("presence mismatch: expected error %v, got error %v", test.err, err) + } + + // Check output matching only if no error was expected/occurred + if err == nil && test.err == nil && out != test.output { + t.Fatalf("output mismatch: expected '%v', got '%v'", test.output, out) + } + }) + } +} + +func TestDateTimeParser_ParseDateTime(t *testing.T) { + // Pre-create some parsers with known Go layouts + parser1 := New([]string{"2006-01-02", "01/02/2006"}) // YYYY-MM-DD, MM/DD/YYYY + parser2 := New([]string{"15:04:05"}) // HH:MM:SS + parserEmpty := New([]string{}) // No layouts + + // Define expected time values + time1, _ := time.Parse("2006-01-02", "2023-10-27") + time2, _ := time.Parse("01/02/2006", "10/27/2023") + time3, _ := time.Parse("15:04:05", "14:30:00") + + tests := []struct { + name string + parser *DateTimeParser + input string + expectTime time.Time + expectLayout string + expectErr error + }{ + { + name: "match first layout", + parser: parser1, + input: "2023-10-27", + expectTime: time1, + expectLayout: "2006-01-02", + expectErr: nil, + }, + { + name: "match second layout", + parser: parser1, + input: "10/27/2023", + expectTime: time2, + expectLayout: "01/02/2006", + expectErr: nil, + }, + { + name: "no matching layout", + parser: parser1, + input: "14:30:00", // Matches parser2's layout, not parser1's + expectTime: time.Time{}, + expectLayout: "", + expectErr: analysis.ErrInvalidDateTime, + }, + { + name: "match only layout", + parser: parser2, + input: "14:30:00", + expectTime: time3, + expectLayout: "15:04:05", + expectErr: nil, + }, + { + name: "invalid date format for layout", + parser: parser1, + input: "27-10-2023", // Wrong separators + expectTime: time.Time{}, + expectLayout: "", + expectErr: analysis.ErrInvalidDateTime, // time.Parse fails on all, returns ErrInvalidDateTime + }, + { + name: "empty input", + parser: parser1, + input: "", + expectTime: time.Time{}, + expectLayout: "", + expectErr: analysis.ErrInvalidDateTime, + }, + { + name: "parser with no layouts", + parser: parserEmpty, + input: "2023-10-27", + expectTime: time.Time{}, + expectLayout: "", + expectErr: analysis.ErrInvalidDateTime, + }, + { + name: "not a date string", + parser: parser1, + input: "hello world", + expectTime: time.Time{}, + expectLayout: "", + expectErr: analysis.ErrInvalidDateTime, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotTime, gotLayout, gotErr := test.parser.ParseDateTime(test.input) + + // Check error + if !reflect.DeepEqual(gotErr, test.expectErr) { + t.Fatalf("error mismatch:\nExpected: %v\nGot: %v", test.expectErr, gotErr) + } + + // Check time only if no error expected + if test.expectErr == nil { + if !gotTime.Equal(test.expectTime) { + t.Errorf("time mismatch:\nExpected: %v\nGot: %v", test.expectTime, gotTime) + } + if gotLayout != test.expectLayout { + t.Errorf("layout mismatch:\nExpected: %q\nGot: %q", test.expectLayout, gotLayout) + } + } + }) + } +} + +func TestDateTimeParserConstructor(t *testing.T) { + tests := []struct { + name string + config map[string]interface{} + expectLayouts []string // Expected Go layouts after parsing + expectErr error + }{ + { + name: "valid config with multiple layouts", + config: map[string]interface{}{ + "layouts": []interface{}{"%Y-%m-%d", "%H:%M:%S %Z"}, + }, + expectLayouts: []string{"2006-01-02", "15:04:05 MST"}, + expectErr: nil, + }, + { + name: "valid config with single layout", + config: map[string]interface{}{ + "layouts": []interface{}{"%Y/%m/%d %z:M"}, + }, + expectLayouts: []string{"2006/01/02 Z07:00"}, + expectErr: nil, + }, + { + name: "valid config with complex layout", + config: map[string]interface{}{ + "layouts": []interface{}{"%a, %d %b %Y %H:%M:%S %zH"}, + }, + expectLayouts: []string{"Mon, 02 Jan 2006 15:04:05 Z07"}, + expectErr: nil, + }, + { + name: "config missing layouts key", + config: map[string]interface{}{ + "other_key": "value", + }, + expectLayouts: nil, + expectErr: fmt.Errorf("must specify layouts"), + }, + { + name: "config layouts not a slice", + config: map[string]interface{}{ + "layouts": "not-a-slice", // Value is a string + }, + expectLayouts: nil, + // Update the expected error message + expectErr: fmt.Errorf("must specify layouts"), + }, + { + name: "config layouts contains non-string", + config: map[string]interface{}{ + "layouts": []interface{}{"%Y-%m-%d", 123}, + }, + // Should process the valid string, ignore the int + expectLayouts: []string{"2006-01-02"}, + expectErr: nil, + }, + { + name: "config layouts contains invalid percent format", + config: map[string]interface{}{ + "layouts": []interface{}{"%Y-%m-%d", "%x"}, // %x is invalid + }, + expectLayouts: nil, + expectErr: fmt.Errorf("invalid format string, unknown format specifier: x"), + }, + { + name: "config layouts contains format ending in %", + config: map[string]interface{}{ + "layouts": []interface{}{"%Y-%m-%d", "%H:%M:%"}, + }, + expectLayouts: nil, + expectErr: fmt.Errorf("invalid format string, expected character after %%"), + }, + { + name: "config with empty layouts slice", + config: map[string]interface{}{ + "layouts": []interface{}{}, + }, + expectLayouts: []string{}, // Expect an empty slice, not nil + expectErr: nil, + }, + { + name: "nil config", + config: nil, + expectLayouts: nil, + expectErr: fmt.Errorf("must specify layouts"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Cache is not used by this constructor, so nil is fine + parserIntf, err := DateTimeParserConstructor(test.config, nil) + + // Check error + // Use string comparison for errors as they might be created differently + expectedErrStr := "" + if test.expectErr != nil { + expectedErrStr = test.expectErr.Error() + } + actualErrStr := "" + if err != nil { + actualErrStr = err.Error() + } + if expectedErrStr != actualErrStr { + t.Fatalf("error mismatch:\nExpected: %q\nGot: %q", expectedErrStr, actualErrStr) + } + + // Check layouts only if no error expected + if test.expectErr == nil { + // Type assert to access the layouts field + parser, ok := parserIntf.(*DateTimeParser) + if !ok { + t.Fatalf("constructor did not return a *DateTimeParser") + } + if !reflect.DeepEqual(parser.layouts, test.expectLayouts) { + t.Errorf("layouts mismatch:\nExpected: %v\nGot: %v", test.expectLayouts, parser.layouts) + } + } + }) + } +} diff --git a/analysis/datetime/sanitized/sanitized.go b/analysis/datetime/sanitized/sanitized.go new file mode 100644 index 0000000..87ad6c7 --- /dev/null +++ b/analysis/datetime/sanitized/sanitized.go @@ -0,0 +1,130 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sanitized + +import ( + "fmt" + "regexp" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "sanitizedgo" + +var validMagicNumbers = map[string]struct{}{ + "2006": {}, + "06": {}, // Year + "01": {}, + "1": {}, + "_1": {}, + "January": {}, + "Jan": {}, // Month + "02": {}, + "2": {}, + "_2": {}, + "__2": {}, + "002": {}, + "Monday": {}, + "Mon": {}, // Day + "15": {}, + "3": {}, + "03": {}, // Hour + "4": {}, + "04": {}, // Minute + "5": {}, + "05": {}, // Second + "0700": {}, + "070000": {}, + "07": {}, + "00": {}, + "": {}, +} + +var layoutSplitRegex = regexp.MustCompile("[\\+\\-= :T,Z\\.<>;\\?!`~@#$%\\^&\\*|'\"\\(\\){}\\[\\]/\\\\]") + +var layoutStripRegex = regexp.MustCompile(`PM|pm|\.9+|\.0+|MST`) + +type DateTimeParser struct { + layouts []string +} + +func New(layouts []string) *DateTimeParser { + return &DateTimeParser{ + layouts: layouts, + } +} + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + for _, layout := range p.layouts { + rv, err := time.Parse(layout, input) + if err == nil { + return rv, layout, nil + } + } + return time.Time{}, "", analysis.ErrInvalidDateTime +} + +// date time layouts must be a combination of constants specified in golang time package +// https://pkg.go.dev/time#pkg-constants +// this validation verifies that only these constants are used in the custom layout +// for compatibility with the golang time package +func validateLayout(layout string) bool { + // first we strip out commonly used constants + // such as "PM" which can be present in the layout + // right after a time component, e.g. 03:04PM; + // because regex split cannot separate "03:04PM" into + // "03:04" and "PM". We also strip out ".9+" and ".0+" + // which represent fractional seconds. + layout = layoutStripRegex.ReplaceAllString(layout, "") + // then we split the layout by non-constant characters + // which is a regex and verify that each split is a valid magic number + split := layoutSplitRegex.Split(layout, -1) + for i := range split { + _, found := validMagicNumbers[split[i]] + if !found { + return false + } + } + return true +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + layouts, ok := config["layouts"].([]interface{}) + if !ok { + return nil, fmt.Errorf("must specify layouts") + } + var layoutStrs []string + for _, layout := range layouts { + layoutStr, ok := layout.(string) + if ok { + if !validateLayout(layoutStr) { + return nil, fmt.Errorf("invalid datetime parser layout: %s,"+ + " please refer to https://pkg.go.dev/time#pkg-constants for supported"+ + " layouts", layoutStr) + } + layoutStrs = append(layoutStrs, layoutStr) + } + } + return New(layoutStrs), nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/sanitized/sanitized_test.go b/analysis/datetime/sanitized/sanitized_test.go new file mode 100644 index 0000000..d62e20a --- /dev/null +++ b/analysis/datetime/sanitized/sanitized_test.go @@ -0,0 +1,109 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sanitized + +import ( + "reflect" + "testing" +) + +func TestLayoutValidatorRegex(t *testing.T) { + splitRegexTests := []struct { + input string + output []string + }{ + { + input: "2014-08-03", + output: []string{"2014", "08", "03"}, + }, + { + input: "2014-08-03T15:59:30", + output: []string{"2014", "08", "03", "15", "59", "30"}, + }, + { + input: "2014.08-03 15/59`30", + output: []string{"2014", "08", "03", "15", "59", "30"}, + }, + { + input: "2014/08/03T15:59:30Z08:00", + output: []string{"2014", "08", "03", "15", "59", "30", "08", "00"}, + }, + { + input: "2014\\08|03T15=59.30.999999999+08*00", + output: []string{"2014", "08", "03", "15", "59", "30", "999999999", "08", "00"}, + }, + { + input: "2006-01-02T15:04:05.999999999Z07:00", + output: []string{"2006", "01", "02", "15", "04", "05", "999999999", "07", "00"}, + }, + { + input: "A-B C:DTE,FZG.HJ;K?L!M`N~O@P#Q$R%S^U&V*W|X'Y\"A(B)C{D}E[F]G/H\\I+J=L", + output: []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", + "Q", "R", "S", "U", "V", "W", "X", "Y", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "L"}, + }, + } + regex := layoutSplitRegex + for _, test := range splitRegexTests { + t.Run(test.input, func(t *testing.T) { + actualOutput := regex.Split(test.input, -1) + if !reflect.DeepEqual(actualOutput, test.output) { + t.Fatalf("expected output %v, got %v", test.output, actualOutput) + } + }) + } + + stripRegexTests := []struct { + input string + output string + }{ + { + input: "3PM", + output: "3", + }, + { + input: "3.0PM", + output: "3", + }, + { + input: "3.9AM", + output: "3AM", + }, + { + input: "3.999999999pm", + output: "3", + }, + { + input: "2006-01-02T15:04:05.999999999Z07:00MST", + output: "2006-01-02T15:04:05Z07:00", + }, + { + input: "Jan _2 15:04:05.0000000+07:00MST", + output: "Jan _2 15:04:05+07:00", + }, + { + input: "15:04:05.99PM+07:00MST", + output: "15:04:05+07:00", + }, + } + regex = layoutStripRegex + for _, test := range stripRegexTests { + t.Run(test.input, func(t *testing.T) { + actualOutput := layoutStripRegex.ReplaceAllString(test.input, "") + if !reflect.DeepEqual(actualOutput, test.output) { + t.Fatalf("expected output %v, got %v", test.output, actualOutput) + } + }) + } +} diff --git a/analysis/datetime/timestamp/microseconds/microseconds.go b/analysis/datetime/timestamp/microseconds/microseconds.go new file mode 100644 index 0000000..88cde75 --- /dev/null +++ b/analysis/datetime/timestamp/microseconds/microseconds.go @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package microseconds + +import ( + "math" + "strconv" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "unix_micro" + +type DateTimeParser struct { +} + +var minBound int64 = math.MinInt64 / 1000 +var maxBound int64 = math.MaxInt64 / 1000 + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + // unix timestamp is milliseconds since UNIX epoch + timestamp, err := strconv.ParseInt(input, 10, 64) + if err != nil { + return time.Time{}, "", analysis.ErrInvalidTimestampString + } + if timestamp < minBound || timestamp > maxBound { + return time.Time{}, "", analysis.ErrInvalidTimestampRange + } + return time.UnixMicro(timestamp), Name, nil +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + return &DateTimeParser{}, nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/timestamp/milliseconds/milliseconds.go b/analysis/datetime/timestamp/milliseconds/milliseconds.go new file mode 100644 index 0000000..645c525 --- /dev/null +++ b/analysis/datetime/timestamp/milliseconds/milliseconds.go @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package milliseconds + +import ( + "math" + "strconv" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "unix_milli" + +type DateTimeParser struct { +} + +var minBound int64 = math.MinInt64 / 1000000 +var maxBound int64 = math.MaxInt64 / 1000000 + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + // unix timestamp is milliseconds since UNIX epoch + timestamp, err := strconv.ParseInt(input, 10, 64) + if err != nil { + return time.Time{}, "", analysis.ErrInvalidTimestampString + } + if timestamp < minBound || timestamp > maxBound { + return time.Time{}, "", analysis.ErrInvalidTimestampRange + } + return time.UnixMilli(timestamp), Name, nil +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + return &DateTimeParser{}, nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/timestamp/nanoseconds/nanoseconds.go b/analysis/datetime/timestamp/nanoseconds/nanoseconds.go new file mode 100644 index 0000000..f50eac1 --- /dev/null +++ b/analysis/datetime/timestamp/nanoseconds/nanoseconds.go @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nanoseconds + +import ( + "math" + "strconv" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "unix_nano" + +type DateTimeParser struct { +} + +var minBound int64 = math.MinInt64 +var maxBound int64 = math.MaxInt64 + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + // unix timestamp is milliseconds since UNIX epoch + timestamp, err := strconv.ParseInt(input, 10, 64) + if err != nil { + return time.Time{}, "", analysis.ErrInvalidTimestampString + } + if timestamp < minBound || timestamp > maxBound { + return time.Time{}, "", analysis.ErrInvalidTimestampRange + } + return time.Unix(0, timestamp), Name, nil +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + return &DateTimeParser{}, nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/datetime/timestamp/seconds/seconds.go b/analysis/datetime/timestamp/seconds/seconds.go new file mode 100644 index 0000000..10219c9 --- /dev/null +++ b/analysis/datetime/timestamp/seconds/seconds.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package seconds + +import ( + "math" + "strconv" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "unix_sec" + +type DateTimeParser struct { +} + +var minBound int64 = math.MinInt64 / 1000000000 +var maxBound int64 = math.MaxInt64 / 1000000000 + +func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) { + // unix timestamp is seconds since UNIX epoch + timestamp, err := strconv.ParseInt(input, 10, 64) + if err != nil { + return time.Time{}, "", analysis.ErrInvalidTimestampString + } + if timestamp < minBound || timestamp > maxBound { + return time.Time{}, "", analysis.ErrInvalidTimestampRange + } + return time.Unix(timestamp, 0), Name, nil +} + +func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) { + return &DateTimeParser{}, nil +} + +func init() { + err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/freq.go b/analysis/freq.go new file mode 100644 index 0000000..a0fd1a4 --- /dev/null +++ b/analysis/freq.go @@ -0,0 +1,70 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis + +import ( + index "github.com/blevesearch/bleve_index_api" +) + +func TokenFrequency(tokens TokenStream, arrayPositions []uint64, options index.FieldIndexingOptions) index.TokenFrequencies { + rv := make(map[string]*index.TokenFreq, len(tokens)) + + if options.IncludeTermVectors() { + tls := make([]index.TokenLocation, len(tokens)) + tlNext := 0 + + for _, token := range tokens { + tls[tlNext] = index.TokenLocation{ + ArrayPositions: arrayPositions, + Start: token.Start, + End: token.End, + Position: token.Position, + } + + curr, ok := rv[string(token.Term)] + if ok { + curr.Locations = append(curr.Locations, &tls[tlNext]) + } else { + curr = &index.TokenFreq{ + Term: token.Term, + Locations: []*index.TokenLocation{&tls[tlNext]}, + } + rv[string(token.Term)] = curr + } + + if !options.SkipFreqNorm() { + curr.SetFrequency(curr.Frequency() + 1) + } + + tlNext++ + } + } else { + for _, token := range tokens { + curr, exists := rv[string(token.Term)] + if !exists { + curr = &index.TokenFreq{ + Term: token.Term, + } + rv[string(token.Term)] = curr + } + + if !options.SkipFreqNorm() { + curr.SetFrequency(curr.Frequency() + 1) + } + } + } + + return rv +} diff --git a/analysis/freq_test.go b/analysis/freq_test.go new file mode 100644 index 0000000..db8f32b --- /dev/null +++ b/analysis/freq_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis + +import ( + index "github.com/blevesearch/bleve_index_api" + "reflect" + "testing" +) + +func TestTokenFrequency(t *testing.T) { + tokens := TokenStream{ + &Token{ + Term: []byte("water"), + Position: 1, + Start: 0, + End: 5, + }, + &Token{ + Term: []byte("water"), + Position: 2, + Start: 6, + End: 11, + }, + } + expectedResult := index.TokenFrequencies{ + "water": &index.TokenFreq{ + Term: []byte("water"), + Locations: []*index.TokenLocation{ + { + Position: 1, + Start: 0, + End: 5, + }, + { + Position: 2, + Start: 6, + End: 11, + }, + }, + }, + } + expectedResult["water"].SetFrequency(2) + result := TokenFrequency(tokens, nil, index.IncludeTermVectors) + if !reflect.DeepEqual(result, expectedResult) { + t.Errorf("expected %#v, got %#v", expectedResult, result) + } +} diff --git a/analysis/lang/ar/analyzer_ar.go b/analysis/lang/ar/analyzer_ar.go new file mode 100644 index 0000000..f6bb36b --- /dev/null +++ b/analysis/lang/ar/analyzer_ar.go @@ -0,0 +1,68 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ar + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "ar" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + normalizeFilter := unicodenorm.MustNewUnicodeNormalizeFilter(unicodenorm.NFKC) + stopArFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + normalizeArFilter, err := cache.TokenFilterNamed(NormalizeName) + if err != nil { + return nil, err + } + stemmerArFilter, err := cache.TokenFilterNamed(StemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + normalizeFilter, + stopArFilter, + normalizeArFilter, + stemmerArFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ar/analyzer_ar_test.go b/analysis/lang/ar/analyzer_ar_test.go new file mode 100644 index 0000000..437d69f --- /dev/null +++ b/analysis/lang/ar/analyzer_ar_test.go @@ -0,0 +1,184 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ar + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestArabicAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + input: []byte("كبير"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كبير"), + Position: 1, + Start: 0, + End: 8, + }, + }, + }, + // feminine marker + { + input: []byte("كبيرة"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كبير"), + Position: 1, + Start: 0, + End: 10, + }, + }, + }, + { + input: []byte("مشروب"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("مشروب"), + Position: 1, + Start: 0, + End: 10, + }, + }, + }, + // plural -at + { + input: []byte("مشروبات"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("مشروب"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + // plural -in + { + input: []byte("أمريكيين"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("امريك"), + Position: 1, + Start: 0, + End: 16, + }, + }, + }, + // singular with bare alif + { + input: []byte("امريكي"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("امريك"), + Position: 1, + Start: 0, + End: 12, + }, + }, + }, + { + input: []byte("كتاب"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كتاب"), + Position: 1, + Start: 0, + End: 8, + }, + }, + }, + // definite article + { + input: []byte("الكتاب"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كتاب"), + Position: 1, + Start: 0, + End: 12, + }, + }, + }, + { + input: []byte("ما ملكت أيمانكم"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ملكت"), + Position: 2, + Start: 5, + End: 13, + }, + &analysis.Token{ + Term: []byte("ايمانكم"), + Position: 3, + Start: 14, + End: 28, + }, + }, + }, + // stopwords + { + input: []byte("الذين ملكت أيمانكم"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ملكت"), + Position: 2, + Start: 11, + End: 19, + }, + &analysis.Token{ + Term: []byte("ايمانكم"), + Position: 3, + Start: 20, + End: 34, + }, + }, + }, + // presentation form normalization + { + input: []byte("ﺍﻟﺴﻼﻢ"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سلام"), + Position: 1, + Start: 0, + End: 15, + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + t.Errorf("expected % x, got % x", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/ar/arabic_normalize.go b/analysis/lang/ar/arabic_normalize.go new file mode 100644 index 0000000..d737c14 --- /dev/null +++ b/analysis/lang/ar/arabic_normalize.go @@ -0,0 +1,88 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ar + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const NormalizeName = "normalize_ar" + +const ( + Alef = '\u0627' + AlefMadda = '\u0622' + AlefHamzaAbove = '\u0623' + AlefHamzaBelow = '\u0625' + Yeh = '\u064A' + DotlessYeh = '\u0649' + TehMarbuta = '\u0629' + Heh = '\u0647' + Tatweel = '\u0640' + Fathatan = '\u064B' + Dammatan = '\u064C' + Kasratan = '\u064D' + Fatha = '\u064E' + Damma = '\u064F' + Kasra = '\u0650' + Shadda = '\u0651' + Sukun = '\u0652' +) + +type ArabicNormalizeFilter struct { +} + +func NewArabicNormalizeFilter() *ArabicNormalizeFilter { + return &ArabicNormalizeFilter{} +} + +func (s *ArabicNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := normalize(token.Term) + token.Term = term + } + return input +} + +func normalize(input []byte) []byte { + runes := bytes.Runes(input) + for i := 0; i < len(runes); i++ { + switch runes[i] { + case AlefMadda, AlefHamzaAbove, AlefHamzaBelow: + runes[i] = Alef + case DotlessYeh: + runes[i] = Yeh + case TehMarbuta: + runes[i] = Heh + case Tatweel, Kasratan, Dammatan, Fathatan, Fatha, Damma, Kasra, Shadda, Sukun: + runes = analysis.DeleteRune(runes, i) + i-- + } + } + return analysis.BuildTermFromRunes(runes) +} + +func NormalizerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewArabicNormalizeFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(NormalizeName, NormalizerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ar/arabic_normalize_test.go b/analysis/lang/ar/arabic_normalize_test.go new file mode 100644 index 0000000..e3f472e --- /dev/null +++ b/analysis/lang/ar/arabic_normalize_test.go @@ -0,0 +1,234 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ar + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestArabicNormalizeFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // AlifMadda + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("آجن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("اجن"), + }, + }, + }, + // AlifHamzaAbove + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("أحمد"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("احمد"), + }, + }, + }, + // AlifHamzaBelow + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("إعاذ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("اعاذ"), + }, + }, + }, + // AlifMaksura + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("بنى"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("بني"), + }, + }, + }, + // TehMarbuta + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("فاطمة"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("فاطمه"), + }, + }, + }, + // Tatweel + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("روبرـــــت"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("روبرت"), + }, + }, + }, + // Fatha + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("مَبنا"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("مبنا"), + }, + }, + }, + // Kasra + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("علِي"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("علي"), + }, + }, + }, + // Damma + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("بُوات"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("بوات"), + }, + }, + }, + // Fathatan + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ولداً"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ولدا"), + }, + }, + }, + // Kasratan + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ولدٍ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ولد"), + }, + }, + }, + // Dammatan + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ولدٌ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ولد"), + }, + }, + }, + // Sukun + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("نلْسون"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("نلسون"), + }, + }, + }, + // Shaddah + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هتميّ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هتمي"), + }, + }, + }, + // empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + arabicNormalizeFilter := NewArabicNormalizeFilter() + for _, test := range tests { + actual := arabicNormalizeFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected % x, got % x", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/ar/stemmer_ar.go b/analysis/lang/ar/stemmer_ar.go new file mode 100644 index 0000000..0b9f96f --- /dev/null +++ b/analysis/lang/ar/stemmer_ar.go @@ -0,0 +1,121 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ar + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StemmerName = "stemmer_ar" + +// These were obtained from org.apache.lucene.analysis.ar.ArabicStemmer +var prefixes = [][]rune{ + []rune("ال"), + []rune("وال"), + []rune("بال"), + []rune("كال"), + []rune("فال"), + []rune("لل"), + []rune("و"), +} +var suffixes = [][]rune{ + []rune("ها"), + []rune("ان"), + []rune("ات"), + []rune("ون"), + []rune("ين"), + []rune("يه"), + []rune("ية"), + []rune("ه"), + []rune("ة"), + []rune("ي"), +} + +type ArabicStemmerFilter struct{} + +func NewArabicStemmerFilter() *ArabicStemmerFilter { + return &ArabicStemmerFilter{} +} + +func (s *ArabicStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := stem(token.Term) + token.Term = term + } + return input +} + +func canStemPrefix(input, prefix []rune) bool { + // Wa- prefix requires at least 3 characters. + if len(prefix) == 1 && len(input) < 4 { + return false + } + // Other prefixes require only 2. + if len(input)-len(prefix) < 2 { + return false + } + for i := range prefix { + if prefix[i] != input[i] { + return false + } + } + return true +} + +func canStemSuffix(input, suffix []rune) bool { + // All suffixes require at least 2 characters after stemming. + if len(input)-len(suffix) < 2 { + return false + } + stemEnd := len(input) - len(suffix) + for i := range suffix { + if suffix[i] != input[stemEnd+i] { + return false + } + } + return true +} + +func stem(input []byte) []byte { + runes := bytes.Runes(input) + // Strip a single prefix. + for _, p := range prefixes { + if canStemPrefix(runes, p) { + runes = runes[len(p):] + break + } + } + // Strip off multiple suffixes, in their order in the suffixes array. + for _, s := range suffixes { + if canStemSuffix(runes, s) { + runes = runes[:len(runes)-len(s)] + } + } + return analysis.BuildTermFromRunes(runes) +} + +func StemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewArabicStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(StemmerName, StemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ar/stemmer_ar_test.go b/analysis/lang/ar/stemmer_ar_test.go new file mode 100644 index 0000000..dfc1e82 --- /dev/null +++ b/analysis/lang/ar/stemmer_ar_test.go @@ -0,0 +1,397 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ar + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestArabicStemmerFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // AlPrefix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("الحسن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("حسن"), + }, + }, + }, + // WalPrefix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("والحسن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("حسن"), + }, + }, + }, + // BalPrefix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("بالحسن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("حسن"), + }, + }, + }, + // KalPrefix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كالحسن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("حسن"), + }, + }, + }, + // FalPrefix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("فالحسن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("حسن"), + }, + }, + }, + // LlPrefix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("للاخر"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("اخر"), + }, + }, + }, + // WaPrefix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("وحسن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("حسن"), + }, + }, + }, + // AhSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("زوجها"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("زوج"), + }, + }, + }, + // AnSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدان"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // AtSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدات"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // WnSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدون"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // YnSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدين"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // YhSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهديه"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // YpSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدية"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // HSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهده"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // PSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدة"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // YSuffix + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدي"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // ComboPrefSuf + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("وساهدون"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // ComboSuf + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهدهات"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ساهد"), + }, + }, + }, + // Shouldn't Stem + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("الو"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("الو"), + }, + }, + }, + // NonArabic + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("English"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("English"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سلام"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سلام"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("السلام"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سلام"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سلامة"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سلام"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("السلامة"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سلام"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("الوصل"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("وصل"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("والصل"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("صل"), + }, + }, + }, + // Empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + arabicStemmerFilter := NewArabicStemmerFilter() + for _, test := range tests { + actual := arabicStemmerFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected % x, got % x", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/ar/stop_filter_ar.go b/analysis/lang/ar/stop_filter_ar.go new file mode 100644 index 0000000..2c548f4 --- /dev/null +++ b/analysis/lang/ar/stop_filter_ar.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ar + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ar/stop_words_ar.go b/analysis/lang/ar/stop_words_ar.go new file mode 100644 index 0000000..8f23282 --- /dev/null +++ b/analysis/lang/ar/stop_words_ar.go @@ -0,0 +1,152 @@ +package ar + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_ar" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis +// ` was changed to ' to allow for literal string + +var ArabicStopWords = []byte(`# This file was created by Jacques Savoy and is distributed under the BSD license. +# See http://members.unine.ch/jacques.savoy/clef/index.html. +# Also see http://www.opensource.org/licenses/bsd-license.html +# Cleaned on October 11, 2009 (not normalized, so use before normalization) +# This means that when modifying this list, you might need to add some +# redundant entries, for example containing forms with both أ and ا +من +ومن +منها +منه +في +وفي +فيها +فيه +و +ف +ثم +او +أو +ب +بها +به +ا +أ +اى +اي +أي +أى +لا +ولا +الا +ألا +إلا +لكن +ما +وما +كما +فما +عن +مع +اذا +إذا +ان +أن +إن +انها +أنها +إنها +انه +أنه +إنه +بان +بأن +فان +فأن +وان +وأن +وإن +التى +التي +الذى +الذي +الذين +الى +الي +إلى +إلي +على +عليها +عليه +اما +أما +إما +ايضا +أيضا +كل +وكل +لم +ولم +لن +ولن +هى +هي +هو +وهى +وهي +وهو +فهى +فهي +فهو +انت +أنت +لك +لها +له +هذه +هذا +تلك +ذلك +هناك +كانت +كان +يكون +تكون +وكانت +وكان +غير +بعض +قد +نحو +بين +بينما +منذ +ضمن +حيث +الان +الآن +خلال +بعد +قبل +حتى +عند +عندما +لدى +جميع +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(ArabicStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/bg/stop_filter_bg.go b/analysis/lang/bg/stop_filter_bg.go new file mode 100644 index 0000000..cd4eabf --- /dev/null +++ b/analysis/lang/bg/stop_filter_bg.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bg + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/bg/stop_words_bg.go b/analysis/lang/bg/stop_words_bg.go new file mode 100644 index 0000000..fde5143 --- /dev/null +++ b/analysis/lang/bg/stop_words_bg.go @@ -0,0 +1,220 @@ +package bg + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_bg" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var BulgarianStopWords = []byte(`# This file was created by Jacques Savoy and is distributed under the BSD license. +# See http://members.unine.ch/jacques.savoy/clef/index.html. +# Also see http://www.opensource.org/licenses/bsd-license.html +а +аз +ако +ала +бе +без +беше +би +бил +била +били +било +близо +бъдат +бъде +бяха +в +вас +ваш +ваша +вероятно +вече +взема +ви +вие +винаги +все +всеки +всички +всичко +всяка +във +въпреки +върху +г +ги +главно +го +д +да +дали +до +докато +докога +дори +досега +доста +е +едва +един +ето +за +зад +заедно +заради +засега +затова +защо +защото +и +из +или +им +има +имат +иска +й +каза +как +каква +какво +както +какъв +като +кога +когато +което +които +кой +който +колко +която +къде +където +към +ли +м +ме +между +мен +ми +мнозина +мога +могат +може +моля +момента +му +н +на +над +назад +най +направи +напред +например +нас +не +него +нея +ни +ние +никой +нито +но +някои +някой +няма +обаче +около +освен +особено +от +отгоре +отново +още +пак +по +повече +повечето +под +поне +поради +после +почти +прави +пред +преди +през +при +пък +първо +с +са +само +се +сега +си +скоро +след +сме +според +сред +срещу +сте +съм +със +също +т +тази +така +такива +такъв +там +твой +те +тези +ти +тн +то +това +тогава +този +той +толкова +точно +трябва +тук +тъй +тя +тях +у +харесва +ч +че +често +чрез +ще +щом +я +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(BulgarianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ca/articles_ca.go b/analysis/lang/ca/articles_ca.go new file mode 100644 index 0000000..51967e7 --- /dev/null +++ b/analysis/lang/ca/articles_ca.go @@ -0,0 +1,33 @@ +package ca + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const ArticlesName = "articles_ca" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis + +var CatalanArticles = []byte(` +d +l +m +n +s +t +`) + +func ArticlesTokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(CatalanArticles) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(ArticlesName, ArticlesTokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ca/elision_ca.go b/analysis/lang/ca/elision_ca.go new file mode 100644 index 0000000..5c83e37 --- /dev/null +++ b/analysis/lang/ca/elision_ca.go @@ -0,0 +1,40 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ca + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/elision" + "github.com/blevesearch/bleve/v2/registry" +) + +const ElisionName = "elision_ca" + +func ElisionFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + articlesTokenMap, err := cache.TokenMapNamed(ArticlesName) + if err != nil { + return nil, fmt.Errorf("error building elision filter: %v", err) + } + return elision.NewElisionFilter(articlesTokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(ElisionName, ElisionFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ca/elision_ca_test.go b/analysis/lang/ca/elision_ca_test.go new file mode 100644 index 0000000..2a9d6a5 --- /dev/null +++ b/analysis/lang/ca/elision_ca_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ca + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestFrenchElision(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("l'Institut"), + }, + &analysis.Token{ + Term: []byte("d'Estudis"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Institut"), + }, + &analysis.Token{ + Term: []byte("Estudis"), + }, + }, + }, + } + + cache := registry.NewCache() + elisionFilter, err := cache.TokenFilterNamed(ElisionName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := elisionFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/ca/stop_filter_ca.go b/analysis/lang/ca/stop_filter_ca.go new file mode 100644 index 0000000..1597bfb --- /dev/null +++ b/analysis/lang/ca/stop_filter_ca.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ca + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ca/stop_words_ca.go b/analysis/lang/ca/stop_words_ca.go new file mode 100644 index 0000000..bfa1be7 --- /dev/null +++ b/analysis/lang/ca/stop_words_ca.go @@ -0,0 +1,247 @@ +package ca + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_ca" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var CatalanStopWords = []byte(`# Catalan stopwords from http://github.com/vcl/cue.language (Apache 2 Licensed) +a +abans +ací +ah +així +això +al +als +aleshores +algun +alguna +algunes +alguns +alhora +allà +allí +allò +altra +altre +altres +amb +ambdós +ambdues +apa +aquell +aquella +aquelles +aquells +aquest +aquesta +aquestes +aquests +aquí +baix +cada +cadascú +cadascuna +cadascunes +cadascuns +com +contra +d'un +d'una +d'unes +d'uns +dalt +de +del +dels +des +després +dins +dintre +donat +doncs +durant +e +eh +el +els +em +en +encara +ens +entre +érem +eren +éreu +es +és +esta +està +estàvem +estaven +estàveu +esteu +et +etc +ets +fins +fora +gairebé +ha +han +has +havia +he +hem +heu +hi +ho +i +igual +iguals +ja +l'hi +la +les +li +li'n +llavors +m'he +ma +mal +malgrat +mateix +mateixa +mateixes +mateixos +me +mentre +més +meu +meus +meva +meves +molt +molta +moltes +molts +mon +mons +n'he +n'hi +ne +ni +no +nogensmenys +només +nosaltres +nostra +nostre +nostres +o +oh +oi +on +pas +pel +pels +per +però +perquè +poc +poca +pocs +poques +potser +propi +qual +quals +quan +quant +que +què +quelcom +qui +quin +quina +quines +quins +s'ha +s'han +sa +semblant +semblants +ses +seu +seus +seva +seva +seves +si +sobre +sobretot +sóc +solament +sols +son +són +sons +sota +sou +t'ha +t'han +t'he +ta +tal +també +tampoc +tan +tant +tanta +tantes +teu +teus +teva +teves +ton +tons +tot +tota +totes +tots +un +una +unes +uns +us +va +vaig +vam +van +vas +veu +vosaltres +vostra +vostre +vostres +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(CatalanStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/cjk/analyzer_cjk.go b/analysis/lang/cjk/analyzer_cjk.go new file mode 100644 index 0000000..ed9d87b --- /dev/null +++ b/analysis/lang/cjk/analyzer_cjk.go @@ -0,0 +1,60 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cjk + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "cjk" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + widthFilter, err := cache.TokenFilterNamed(WidthName) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + bigramFilter, err := cache.TokenFilterNamed(BigramName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + widthFilter, + toLowerFilter, + bigramFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/cjk/analyzer_cjk_test.go b/analysis/lang/cjk/analyzer_cjk_test.go new file mode 100644 index 0000000..afd8957 --- /dev/null +++ b/analysis/lang/cjk/analyzer_cjk_test.go @@ -0,0 +1,642 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cjk + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestCJKAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + input: []byte("こんにちは世界"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こん"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("んに"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("にち"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("ちは"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("は世"), + Type: analysis.Double, + Position: 5, + Start: 12, + End: 18, + }, + &analysis.Token{ + Term: []byte("世界"), + Type: analysis.Double, + Position: 6, + Start: 15, + End: 21, + }, + }, + }, + { + input: []byte("一二三四五六七八九十"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("一二"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("二三"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("三四"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("四五"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("五六"), + Type: analysis.Double, + Position: 5, + Start: 12, + End: 18, + }, + &analysis.Token{ + Term: []byte("六七"), + Type: analysis.Double, + Position: 6, + Start: 15, + End: 21, + }, + &analysis.Token{ + Term: []byte("七八"), + Type: analysis.Double, + Position: 7, + Start: 18, + End: 24, + }, + &analysis.Token{ + Term: []byte("八九"), + Type: analysis.Double, + Position: 8, + Start: 21, + End: 27, + }, + &analysis.Token{ + Term: []byte("九十"), + Type: analysis.Double, + Position: 9, + Start: 24, + End: 30, + }, + }, + }, + { + input: []byte("一 二三四 五六七八九 十"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("一"), + Type: analysis.Single, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("二三"), + Type: analysis.Double, + Position: 2, + Start: 4, + End: 10, + }, + &analysis.Token{ + Term: []byte("三四"), + Type: analysis.Double, + Position: 3, + Start: 7, + End: 13, + }, + &analysis.Token{ + Term: []byte("五六"), + Type: analysis.Double, + Position: 4, + Start: 14, + End: 20, + }, + &analysis.Token{ + Term: []byte("六七"), + Type: analysis.Double, + Position: 5, + Start: 17, + End: 23, + }, + &analysis.Token{ + Term: []byte("七八"), + Type: analysis.Double, + Position: 6, + Start: 20, + End: 26, + }, + &analysis.Token{ + Term: []byte("八九"), + Type: analysis.Double, + Position: 7, + Start: 23, + End: 29, + }, + &analysis.Token{ + Term: []byte("十"), + Type: analysis.Single, + Position: 8, + Start: 30, + End: 33, + }, + }, + }, + { + input: []byte("abc defgh ijklmn opqrstu vwxy z"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abc"), + Type: analysis.AlphaNumeric, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("defgh"), + Type: analysis.AlphaNumeric, + Position: 2, + Start: 4, + End: 9, + }, + &analysis.Token{ + Term: []byte("ijklmn"), + Type: analysis.AlphaNumeric, + Position: 3, + Start: 10, + End: 16, + }, + &analysis.Token{ + Term: []byte("opqrstu"), + Type: analysis.AlphaNumeric, + Position: 4, + Start: 17, + End: 24, + }, + &analysis.Token{ + Term: []byte("vwxy"), + Type: analysis.AlphaNumeric, + Position: 5, + Start: 25, + End: 29, + }, + &analysis.Token{ + Term: []byte("z"), + Type: analysis.AlphaNumeric, + Position: 6, + Start: 30, + End: 31, + }, + }, + }, + { + input: []byte("あい"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("あい"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + }, + }, + { + input: []byte("あい "), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("あい"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + }, + }, + { + input: []byte("test"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("test"), + Type: analysis.AlphaNumeric, + Position: 1, + Start: 0, + End: 4, + }, + }, + }, + { + input: []byte("test "), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("test"), + Type: analysis.AlphaNumeric, + Position: 1, + Start: 0, + End: 4, + }, + }, + }, + { + input: []byte("あいtest"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("あい"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("test"), + Type: analysis.AlphaNumeric, + Position: 2, + Start: 6, + End: 10, + }, + }, + }, + { + input: []byte("testあい "), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("test"), + Type: analysis.AlphaNumeric, + Position: 1, + Start: 0, + End: 4, + }, + &analysis.Token{ + Term: []byte("あい"), + Type: analysis.Double, + Position: 2, + Start: 4, + End: 10, + }, + }, + }, + { + input: []byte("あいうえおabcかきくけこ"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("あい"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("いう"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("うえ"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("えお"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("abc"), + Type: analysis.AlphaNumeric, + Position: 5, + Start: 15, + End: 18, + }, + &analysis.Token{ + Term: []byte("かき"), + Type: analysis.Double, + Position: 6, + Start: 18, + End: 24, + }, + &analysis.Token{ + Term: []byte("きく"), + Type: analysis.Double, + Position: 7, + Start: 21, + End: 27, + }, + &analysis.Token{ + Term: []byte("くけ"), + Type: analysis.Double, + Position: 8, + Start: 24, + End: 30, + }, + &analysis.Token{ + Term: []byte("けこ"), + Type: analysis.Double, + Position: 9, + Start: 27, + End: 33, + }, + }, + }, + { + input: []byte("あいうえおabんcかきくけ こ"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("あい"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("いう"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("うえ"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("えお"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("ab"), + Type: analysis.AlphaNumeric, + Position: 5, + Start: 15, + End: 17, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Single, + Position: 6, + Start: 17, + End: 20, + }, + &analysis.Token{ + Term: []byte("c"), + Type: analysis.AlphaNumeric, + Position: 7, + Start: 20, + End: 21, + }, + &analysis.Token{ + Term: []byte("かき"), + Type: analysis.Double, + Position: 8, + Start: 21, + End: 27, + }, + &analysis.Token{ + Term: []byte("きく"), + Type: analysis.Double, + Position: 9, + Start: 24, + End: 30, + }, + &analysis.Token{ + Term: []byte("くけ"), + Type: analysis.Double, + Position: 10, + Start: 27, + End: 33, + }, + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Single, + Position: 11, + Start: 34, + End: 37, + }, + }, + }, + { + input: []byte("一 روبرت موير"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("一"), + Type: analysis.Single, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("روبرت"), + Type: analysis.AlphaNumeric, + Position: 2, + Start: 4, + End: 14, + }, + &analysis.Token{ + Term: []byte("موير"), + Type: analysis.AlphaNumeric, + Position: 3, + Start: 15, + End: 23, + }, + }, + }, + { + input: []byte("一 رُوبرت موير"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("一"), + Type: analysis.Single, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("رُوبرت"), + Type: analysis.AlphaNumeric, + Position: 2, + Start: 4, + End: 16, + }, + &analysis.Token{ + Term: []byte("موير"), + Type: analysis.AlphaNumeric, + Position: 3, + Start: 17, + End: 25, + }, + }, + }, + { + input: []byte("𩬅艱鍟䇹愯瀛"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("𩬅艱"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 7, + }, + &analysis.Token{ + Term: []byte("艱鍟"), + Type: analysis.Double, + Position: 2, + Start: 4, + End: 10, + }, + &analysis.Token{ + Term: []byte("鍟䇹"), + Type: analysis.Double, + Position: 3, + Start: 7, + End: 13, + }, + &analysis.Token{ + Term: []byte("䇹愯"), + Type: analysis.Double, + Position: 4, + Start: 10, + End: 16, + }, + &analysis.Token{ + Term: []byte("愯瀛"), + Type: analysis.Double, + Position: 5, + Start: 13, + End: 19, + }, + }, + }, + { + input: []byte("一"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("一"), + Type: analysis.Single, + Position: 1, + Start: 0, + End: 3, + }, + }, + }, + { + input: []byte("一丁丂"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("一丁"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("丁丂"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + }, + }, + } + + cache := registry.NewCache() + for _, test := range tests { + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + } + } +} + +func BenchmarkCJKAnalyzer(b *testing.B) { + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + b.Fatal(err) + } + + for i := 0; i < b.N; i++ { + analyzer.Analyze(bleveWikiArticleJapanese) + } +} + +var bleveWikiArticleJapanese = []byte(`加圧容器に貯蔵されている液体物質は、その時の気液平衡状態にあるが、火災により容器が加熱されていると容器内の液体は、その物質の大気圧のもとでの沸点より十分に高い温度まで加熱され、圧力も高くなる。この状態で容器が破裂すると容器内部の圧力は瞬間的に大気圧にまで低下する。 +この時に容器内の平衡状態が破られ、液体は突沸し、気体になることで爆発現象を起こす。液化石油ガスなどでは、さらに拡散して空気と混ざったガスが自由空間蒸気雲爆発を起こす。液化石油ガスなどの常温常圧で気体になる物を高い圧力で液化して収納している容器、あるいは、そのような液体を輸送するためのパイプラインや配管などが火災などによって破壊されたときに起きる。 +ブリーブという現象が明らかになったのは、フランス・リヨンの郊外にあるフェザンという町のフェザン製油所(ウニオン・ド・ゼネラル・ド・ペトロール)で大規模な爆発火災事故が発生したときだと言われている。 +中身の液体が高温高圧の水である場合には「水蒸気爆発」と呼ばれる。`) diff --git a/analysis/lang/cjk/cjk_bigram.go b/analysis/lang/cjk/cjk_bigram.go new file mode 100644 index 0000000..b36308d --- /dev/null +++ b/analysis/lang/cjk/cjk_bigram.go @@ -0,0 +1,210 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cjk + +import ( + "bytes" + "container/ring" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const BigramName = "cjk_bigram" + +type CJKBigramFilter struct { + outputUnigram bool +} + +func NewCJKBigramFilter(outputUnigram bool) *CJKBigramFilter { + return &CJKBigramFilter{ + outputUnigram: outputUnigram, + } +} + +func (s *CJKBigramFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + r := ring.New(2) + itemsInRing := 0 + pos := 1 + outputPos := 1 + + rv := make(analysis.TokenStream, 0, len(input)) + + for _, tokout := range input { + if tokout.Type == analysis.Ideographic { + runes := bytes.Runes(tokout.Term) + sofar := 0 + for _, run := range runes { + rlen := utf8.RuneLen(run) + token := &analysis.Token{ + Term: tokout.Term[sofar : sofar+rlen], + Start: tokout.Start + sofar, + End: tokout.Start + sofar + rlen, + Position: pos, + Type: tokout.Type, + KeyWord: tokout.KeyWord, + } + pos++ + sofar += rlen + if itemsInRing > 0 { + // if items already buffered + // check to see if this is aligned + curr := r.Value.(*analysis.Token) + if token.Start-curr.End != 0 { + // not aligned flush + flushToken := s.flush(r, &itemsInRing, outputPos) + if flushToken != nil { + outputPos++ + rv = append(rv, flushToken) + } + } + } + // now we can add this token to the buffer + r = r.Next() + r.Value = token + if itemsInRing < 2 { + itemsInRing++ + } + builtUnigram := false + if itemsInRing > 1 && s.outputUnigram { + unigram := s.buildUnigram(r, &itemsInRing, outputPos) + if unigram != nil { + builtUnigram = true + rv = append(rv, unigram) + } + } + bigramToken := s.outputBigram(r, &itemsInRing, outputPos) + if bigramToken != nil { + rv = append(rv, bigramToken) + outputPos++ + } + + // prev token should be removed if unigram was built + if builtUnigram { + itemsInRing-- + } + } + + } else { + // flush anything already buffered + flushToken := s.flush(r, &itemsInRing, outputPos) + if flushToken != nil { + rv = append(rv, flushToken) + outputPos++ + } + // output this token as is + tokout.Position = outputPos + rv = append(rv, tokout) + outputPos++ + } + } + + // deal with possible trailing unigram + if itemsInRing == 1 || s.outputUnigram { + if itemsInRing == 2 { + r = r.Next() + } + unigram := s.buildUnigram(r, &itemsInRing, outputPos) + if unigram != nil { + rv = append(rv, unigram) + } + } + return rv +} + +func (s *CJKBigramFilter) flush(r *ring.Ring, itemsInRing *int, pos int) *analysis.Token { + var rv *analysis.Token + if *itemsInRing == 1 { + rv = s.buildUnigram(r, itemsInRing, pos) + } + r.Value = nil + *itemsInRing = 0 + + return rv +} + +func (s *CJKBigramFilter) outputBigram(r *ring.Ring, itemsInRing *int, pos int) *analysis.Token { + if *itemsInRing == 2 { + thisShingleRing := r.Move(-1) + shingledBytes := make([]byte, 0) + + // do first token + prev := thisShingleRing.Value.(*analysis.Token) + shingledBytes = append(shingledBytes, prev.Term...) + + // do second token + thisShingleRing = thisShingleRing.Next() + curr := thisShingleRing.Value.(*analysis.Token) + shingledBytes = append(shingledBytes, curr.Term...) + + token := analysis.Token{ + Type: analysis.Double, + Term: shingledBytes, + Position: pos, + Start: prev.Start, + End: curr.End, + } + return &token + } + + return nil +} + +func (s *CJKBigramFilter) buildUnigram(r *ring.Ring, itemsInRing *int, pos int) *analysis.Token { + switch *itemsInRing { + case 2: + thisShingleRing := r.Move(-1) + // do first token + prev := thisShingleRing.Value.(*analysis.Token) + token := analysis.Token{ + Type: analysis.Single, + Term: prev.Term, + Position: pos, + Start: prev.Start, + End: prev.End, + } + return &token + case 1: + // do first token + prev := r.Value.(*analysis.Token) + token := analysis.Token{ + Type: analysis.Single, + Term: prev.Term, + Position: pos, + Start: prev.Start, + End: prev.End, + } + return &token + } + + return nil +} + +func CJKBigramFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + outputUnigram := false + outVal, ok := config["output_unigram"].(bool) + if ok { + outputUnigram = outVal + } + return NewCJKBigramFilter(outputUnigram), nil +} + +func init() { + err := registry.RegisterTokenFilter(BigramName, CJKBigramFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/cjk/cjk_bigram_test.go b/analysis/lang/cjk/cjk_bigram_test.go new file mode 100644 index 0000000..0528eea --- /dev/null +++ b/analysis/lang/cjk/cjk_bigram_test.go @@ -0,0 +1,848 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cjk + +import ( + "container/ring" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +// Helper function to create a token +func makeToken(term string, start, end, pos int) *analysis.Token { + return &analysis.Token{ + Term: []byte(term), + Start: start, + End: end, + Position: pos, // Note: buildUnigram uses the 'pos' argument, not the token's original pos + Type: analysis.Ideographic, + } +} + +func TestCJKBigramFilter_buildUnigram(t *testing.T) { + filter := NewCJKBigramFilter(false) + + tests := []struct { + name string + ringSetup func() (*ring.Ring, int) // Function to set up the ring and itemsInRing + inputPos int // Position to pass to buildUnigram + expectToken *analysis.Token + }{ + { + name: "itemsInRing == 2", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("一", 0, 3, 1) // Original pos 1 + token2 := makeToken("二", 3, 6, 2) // Original pos 2 + r.Value = token1 + r = r.Next() + r.Value = token2 + // r currently points to token2, r.Move(-1) points to token1 + return r, 2 + }, + inputPos: 10, // Expected output position + expectToken: &analysis.Token{ + Type: analysis.Single, + Term: []byte("一"), + Position: 10, // Should use inputPos + Start: 0, + End: 3, + }, + }, + { + name: "itemsInRing == 1 (ring points to the single item)", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("三", 6, 9, 3) + r.Value = token1 + // r points to token1 + return r, 1 + }, + inputPos: 11, + expectToken: &analysis.Token{ + Type: analysis.Single, + Term: []byte("三"), + Position: 11, // Should use inputPos + Start: 6, + End: 9, + }, + }, + { + name: "itemsInRing == 1 (ring points to nil, next is the single item)", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("四", 9, 12, 4) + r = r.Next() // r points to nil initially + r.Value = token1 + // r points to token1 + return r, 1 + }, + inputPos: 12, + expectToken: &analysis.Token{ + Type: analysis.Single, + Term: []byte("四"), + Position: 12, // Should use inputPos + Start: 9, + End: 12, + }, + }, + { + name: "itemsInRing == 0", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + // Ring is empty + return r, 0 + }, + inputPos: 13, + expectToken: nil, // Expect nil when itemsInRing is not 1 or 2 + }, + { + name: "itemsInRing > 2 (should behave like 0)", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("五", 12, 15, 5) + token2 := makeToken("六", 15, 18, 6) + r.Value = token1 + r = r.Next() + r.Value = token2 + // Simulate incorrect itemsInRing count + return r, 3 + }, + inputPos: 14, + expectToken: nil, // Expect nil when itemsInRing is not 1 or 2 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ringPtr, itemsInRing := tt.ringSetup() + itemsInRingCopy := itemsInRing // Pass a pointer to a copy + + gotToken := filter.buildUnigram(ringPtr, &itemsInRingCopy, tt.inputPos) + + if !reflect.DeepEqual(gotToken, tt.expectToken) { + t.Errorf("buildUnigram() got = %v, want %v", gotToken, tt.expectToken) + } + + // Check if itemsInRing was modified (it shouldn't be by buildUnigram) + if itemsInRingCopy != itemsInRing { + t.Errorf("buildUnigram() modified itemsInRing, got = %d, want %d", itemsInRingCopy, itemsInRing) + } + }) + } +} + +func TestCJKBigramFilter_outputBigram(t *testing.T) { + // Create a filter instance (outputUnigram value doesn't matter for outputBigram) + filter := NewCJKBigramFilter(false) + + tests := []struct { + name string + ringSetup func() (*ring.Ring, int) // Function to set up the ring and itemsInRing + inputPos int // Position to pass to outputBigram + expectToken *analysis.Token + }{ + { + name: "itemsInRing == 2", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("一", 0, 3, 1) // Original pos 1 + token2 := makeToken("二", 3, 6, 2) // Original pos 2 + r.Value = token1 + r = r.Next() + r.Value = token2 + // r currently points to token2, r.Move(-1) points to token1 + return r, 2 + }, + inputPos: 10, // Expected output position + expectToken: &analysis.Token{ + Type: analysis.Double, + Term: []byte("一二"), // Combined term + Position: 10, // Should use inputPos + Start: 0, // Start of first token + End: 6, // End of second token + }, + }, + { + name: "itemsInRing == 2 with different terms", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("你好", 0, 6, 1) + token2 := makeToken("世界", 6, 12, 2) + r.Value = token1 + r = r.Next() + r.Value = token2 + return r, 2 + }, + inputPos: 5, + expectToken: &analysis.Token{ + Type: analysis.Double, + Term: []byte("你好世界"), + Position: 5, + Start: 0, + End: 12, + }, + }, + { + name: "itemsInRing == 1", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("三", 6, 9, 3) + r.Value = token1 + return r, 1 + }, + inputPos: 11, + expectToken: nil, // Expect nil when itemsInRing is not 2 + }, + { + name: "itemsInRing == 0", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + // Ring is empty + return r, 0 + }, + inputPos: 13, + expectToken: nil, // Expect nil when itemsInRing is not 2 + }, + { + name: "itemsInRing > 2 (should behave like 0)", + ringSetup: func() (*ring.Ring, int) { + r := ring.New(2) + token1 := makeToken("五", 12, 15, 5) + token2 := makeToken("六", 15, 18, 6) + r.Value = token1 + r = r.Next() + r.Value = token2 + // Simulate incorrect itemsInRing count + return r, 3 + }, + inputPos: 14, + expectToken: nil, // Expect nil when itemsInRing is not 2 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ringPtr, itemsInRing := tt.ringSetup() + itemsInRingCopy := itemsInRing // Pass a pointer to a copy + + gotToken := filter.outputBigram(ringPtr, &itemsInRingCopy, tt.inputPos) + + if !reflect.DeepEqual(gotToken, tt.expectToken) { + t.Errorf("outputBigram() got = %v, want %v", gotToken, tt.expectToken) + } + + // Check if itemsInRing was modified (it shouldn't be by outputBigram) + if itemsInRingCopy != itemsInRing { + t.Errorf("outputBigram() modified itemsInRing, got = %d, want %d", itemsInRingCopy, itemsInRing) + } + }) + } +} + +func TestCJKBigramFilter(t *testing.T) { + tests := []struct { + outputUnigram bool + input analysis.TokenStream + output analysis.TokenStream + }{ + // first test that non-adjacent terms are not combined + { + outputUnigram: false, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Ideographic, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Ideographic, + Position: 2, + Start: 5, + End: 8, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Single, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Single, + Position: 2, + Start: 5, + End: 8, + }, + }, + }, + { + outputUnigram: false, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Ideographic, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Ideographic, + Position: 2, + Start: 3, + End: 6, + }, + &analysis.Token{ + Term: []byte("に"), + Type: analysis.Ideographic, + Position: 3, + Start: 6, + End: 9, + }, + &analysis.Token{ + Term: []byte("ち"), + Type: analysis.Ideographic, + Position: 4, + Start: 9, + End: 12, + }, + &analysis.Token{ + Term: []byte("は"), + Type: analysis.Ideographic, + Position: 5, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("世"), + Type: analysis.Ideographic, + Position: 6, + Start: 15, + End: 18, + }, + &analysis.Token{ + Term: []byte("界"), + Type: analysis.Ideographic, + Position: 7, + Start: 18, + End: 21, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こん"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("んに"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("にち"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("ちは"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("は世"), + Type: analysis.Double, + Position: 5, + Start: 12, + End: 18, + }, + &analysis.Token{ + Term: []byte("世界"), + Type: analysis.Double, + Position: 6, + Start: 15, + End: 21, + }, + }, + }, + { + outputUnigram: true, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Ideographic, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Ideographic, + Position: 2, + Start: 3, + End: 6, + }, + &analysis.Token{ + Term: []byte("に"), + Type: analysis.Ideographic, + Position: 3, + Start: 6, + End: 9, + }, + &analysis.Token{ + Term: []byte("ち"), + Type: analysis.Ideographic, + Position: 4, + Start: 9, + End: 12, + }, + &analysis.Token{ + Term: []byte("は"), + Type: analysis.Ideographic, + Position: 5, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("世"), + Type: analysis.Ideographic, + Position: 6, + Start: 15, + End: 18, + }, + &analysis.Token{ + Term: []byte("界"), + Type: analysis.Ideographic, + Position: 7, + Start: 18, + End: 21, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Single, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("こん"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Single, + Position: 2, + Start: 3, + End: 6, + }, + &analysis.Token{ + Term: []byte("んに"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("に"), + Type: analysis.Single, + Position: 3, + Start: 6, + End: 9, + }, + &analysis.Token{ + Term: []byte("にち"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("ち"), + Type: analysis.Single, + Position: 4, + Start: 9, + End: 12, + }, + &analysis.Token{ + Term: []byte("ちは"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("は"), + Type: analysis.Single, + Position: 5, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("は世"), + Type: analysis.Double, + Position: 5, + Start: 12, + End: 18, + }, + &analysis.Token{ + Term: []byte("世"), + Type: analysis.Single, + Position: 6, + Start: 15, + End: 18, + }, + &analysis.Token{ + Term: []byte("世界"), + Type: analysis.Double, + Position: 6, + Start: 15, + End: 21, + }, + &analysis.Token{ + Term: []byte("界"), + Type: analysis.Single, + Position: 7, + Start: 18, + End: 21, + }, + }, + }, + { + // Assuming that `、` is removed by unicode tokenizer from `こんにちは、世界` + outputUnigram: true, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Ideographic, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Ideographic, + Position: 2, + Start: 3, + End: 6, + }, + &analysis.Token{ + Term: []byte("に"), + Type: analysis.Ideographic, + Position: 3, + Start: 6, + End: 9, + }, + &analysis.Token{ + Term: []byte("ち"), + Type: analysis.Ideographic, + Position: 4, + Start: 9, + End: 12, + }, + &analysis.Token{ + Term: []byte("は"), + Type: analysis.Ideographic, + Position: 5, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("世"), + Type: analysis.Ideographic, + Position: 7, + Start: 18, + End: 21, + }, + &analysis.Token{ + Term: []byte("界"), + Type: analysis.Ideographic, + Position: 8, + Start: 21, + End: 24, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Single, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("こん"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Single, + Position: 2, + Start: 3, + End: 6, + }, + &analysis.Token{ + Term: []byte("んに"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("に"), + Type: analysis.Single, + Position: 3, + Start: 6, + End: 9, + }, + &analysis.Token{ + Term: []byte("にち"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("ち"), + Type: analysis.Single, + Position: 4, + Start: 9, + End: 12, + }, + &analysis.Token{ + Term: []byte("ちは"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("は"), + Type: analysis.Single, + Position: 5, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("世"), + Type: analysis.Single, + Position: 6, + Start: 18, + End: 21, + }, + &analysis.Token{ + Term: []byte("世界"), + Type: analysis.Double, + Position: 6, + Start: 18, + End: 24, + }, + &analysis.Token{ + Term: []byte("界"), + Type: analysis.Single, + Position: 7, + Start: 21, + End: 24, + }, + }, + }, + { + outputUnigram: false, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こ"), + Type: analysis.Ideographic, + Position: 1, + Start: 0, + End: 3, + }, + &analysis.Token{ + Term: []byte("ん"), + Type: analysis.Ideographic, + Position: 2, + Start: 3, + End: 6, + }, + &analysis.Token{ + Term: []byte("に"), + Type: analysis.Ideographic, + Position: 3, + Start: 6, + End: 9, + }, + &analysis.Token{ + Term: []byte("ち"), + Type: analysis.Ideographic, + Position: 4, + Start: 9, + End: 12, + }, + &analysis.Token{ + Term: []byte("は"), + Type: analysis.Ideographic, + Position: 5, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("cat"), + Type: analysis.AlphaNumeric, + Position: 6, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("世"), + Type: analysis.Ideographic, + Position: 7, + Start: 18, + End: 21, + }, + &analysis.Token{ + Term: []byte("界"), + Type: analysis.Ideographic, + Position: 8, + Start: 21, + End: 24, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こん"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("んに"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("にち"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("ちは"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("cat"), + Type: analysis.AlphaNumeric, + Position: 5, + Start: 12, + End: 15, + }, + &analysis.Token{ + Term: []byte("世界"), + Type: analysis.Double, + Position: 6, + Start: 18, + End: 24, + }, + }, + }, + { + outputUnigram: false, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("パイプライン"), + Type: analysis.Ideographic, + Position: 1, + Start: 0, + End: 18, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("パイ"), + Type: analysis.Double, + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("イプ"), + Type: analysis.Double, + Position: 2, + Start: 3, + End: 9, + }, + &analysis.Token{ + Term: []byte("プラ"), + Type: analysis.Double, + Position: 3, + Start: 6, + End: 12, + }, + &analysis.Token{ + Term: []byte("ライ"), + Type: analysis.Double, + Position: 4, + Start: 9, + End: 15, + }, + &analysis.Token{ + Term: []byte("イン"), + Type: analysis.Double, + Position: 5, + Start: 12, + End: 18, + }, + }, + }, + } + + for _, test := range tests { + cjkBigramFilter := NewCJKBigramFilter(test.outputUnigram) + actual := cjkBigramFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + } +} diff --git a/analysis/lang/cjk/cjk_width.go b/analysis/lang/cjk/cjk_width.go new file mode 100644 index 0000000..a9d6171 --- /dev/null +++ b/analysis/lang/cjk/cjk_width.go @@ -0,0 +1,104 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cjk + +import ( + "bytes" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const WidthName = "cjk_width" + +type CJKWidthFilter struct{} + +func NewCJKWidthFilter() *CJKWidthFilter { + return &CJKWidthFilter{} +} + +func (s *CJKWidthFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runeCount := utf8.RuneCount(token.Term) + runes := bytes.Runes(token.Term) + for i := 0; i < runeCount; i++ { + ch := runes[i] + if ch >= 0xFF01 && ch <= 0xFF5E { + // fullwidth ASCII variants + runes[i] -= 0xFEE0 + } else if ch >= 0xFF65 && ch <= 0xFF9F { + // halfwidth Katakana variants + if (ch == 0xFF9E || ch == 0xFF9F) && i > 0 && combine(runes, i, ch) { + runes = analysis.DeleteRune(runes, i) + i-- + runeCount = len(runes) + } else { + runes[i] = kanaNorm[ch-0xFF65] + } + } + } + token.Term = analysis.BuildTermFromRunes(runes) + } + + return input +} + +var kanaNorm = []rune{ + 0x30fb, 0x30f2, 0x30a1, 0x30a3, 0x30a5, 0x30a7, 0x30a9, 0x30e3, 0x30e5, + 0x30e7, 0x30c3, 0x30fc, 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, 0x30ab, + 0x30ad, 0x30af, 0x30b1, 0x30b3, 0x30b5, 0x30b7, 0x30b9, 0x30bb, 0x30bd, + 0x30bf, 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ca, 0x30cb, 0x30cc, 0x30cd, + 0x30ce, 0x30cf, 0x30d2, 0x30d5, 0x30d8, 0x30db, 0x30de, 0x30df, 0x30e0, + 0x30e1, 0x30e2, 0x30e4, 0x30e6, 0x30e8, 0x30e9, 0x30ea, 0x30eb, 0x30ec, + 0x30ed, 0x30ef, 0x30f3, 0x3099, 0x309A, +} + +var kanaCombineVoiced = []rune{ + 78, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, + 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, +} +var kanaCombineHalfVoiced = []rune{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, 0, 0, 2, + 0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +} + +func combine(text []rune, pos int, r rune) bool { + prev := text[pos-1] + if prev >= 0x30A6 && prev <= 0x30FD { + if r == 0xFF9F { + text[pos-1] += kanaCombineHalfVoiced[prev-0x30A6] + } else { + text[pos-1] += kanaCombineVoiced[prev-0x30A6] + } + return text[pos-1] != prev + } + return false +} + +func CJKWidthFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewCJKWidthFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(WidthName, CJKWidthFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/cjk/cjk_width_test.go b/analysis/lang/cjk/cjk_width_test.go new file mode 100644 index 0000000..5533ae2 --- /dev/null +++ b/analysis/lang/cjk/cjk_width_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cjk + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestCJKWidthFilter(t *testing.T) { + + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Test"), + }, + &analysis.Token{ + Term: []byte("1234"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Test"), + }, + &analysis.Token{ + Term: []byte("1234"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("カタカナ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("カタカナ"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ヴィッツ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ヴィッツ"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("パナソニック"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("パナソニック"), + }, + }, + }, + } + + for _, test := range tests { + cjkWidthFilter := NewCJKWidthFilter() + actual := cjkWidthFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + } +} diff --git a/analysis/lang/ckb/analyzer_ckb.go b/analysis/lang/ckb/analyzer_ckb.go new file mode 100644 index 0000000..2b44b57 --- /dev/null +++ b/analysis/lang/ckb/analyzer_ckb.go @@ -0,0 +1,64 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ckb + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + "github.com/blevesearch/bleve/v2/registry" +) + +const AnalyzerName = "ckb" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + normCkbFilter, err := cache.TokenFilterNamed(NormalizeName) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopCkbFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerCkbFilter, err := cache.TokenFilterNamed(StemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + normCkbFilter, + toLowerFilter, + stopCkbFilter, + stemmerCkbFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ckb/analyzer_ckb_test.go b/analysis/lang/ckb/analyzer_ckb_test.go new file mode 100644 index 0000000..9e6adab --- /dev/null +++ b/analysis/lang/ckb/analyzer_ckb_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ckb + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSoraniAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stop word removal + { + input: []byte("ئەم پیاوە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 2, + Start: 7, + End: 17, + }, + }, + }, + { + input: []byte("پیاوە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 1, + Start: 0, + End: 10, + }, + }, + }, + { + input: []byte("پیاو"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 1, + Start: 0, + End: 8, + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + } + } +} diff --git a/analysis/lang/ckb/sorani_normalize.go b/analysis/lang/ckb/sorani_normalize.go new file mode 100644 index 0000000..fade3f5 --- /dev/null +++ b/analysis/lang/ckb/sorani_normalize.go @@ -0,0 +1,121 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ckb + +import ( + "bytes" + "unicode" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const NormalizeName = "normalize_ckb" + +const ( + Yeh = '\u064A' + DotlessYeh = '\u0649' + FarsiYeh = '\u06CC' + + Kaf = '\u0643' + Keheh = '\u06A9' + + Heh = '\u0647' + Ae = '\u06D5' + Zwnj = '\u200C' + HehDoachashmee = '\u06BE' + TehMarbuta = '\u0629' + + Reh = '\u0631' + Rreh = '\u0695' + RrehAbove = '\u0692' + + Tatweel = '\u0640' + Fathatan = '\u064B' + Dammatan = '\u064C' + Kasratan = '\u064D' + Fatha = '\u064E' + Damma = '\u064F' + Kasra = '\u0650' + Shadda = '\u0651' + Sukun = '\u0652' +) + +type SoraniNormalizeFilter struct { +} + +func NewSoraniNormalizeFilter() *SoraniNormalizeFilter { + return &SoraniNormalizeFilter{} +} + +func (s *SoraniNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := normalize(token.Term) + token.Term = term + } + return input +} + +func normalize(input []byte) []byte { + runes := bytes.Runes(input) + for i := 0; i < len(runes); i++ { + switch runes[i] { + case Yeh, DotlessYeh: + runes[i] = FarsiYeh + case Kaf: + runes[i] = Keheh + case Zwnj: + if i > 0 && runes[i-1] == Heh { + runes[i-1] = Ae + } + runes = analysis.DeleteRune(runes, i) + i-- + case Heh: + if i == len(runes)-1 { + runes[i] = Ae + } + case TehMarbuta: + runes[i] = Ae + case HehDoachashmee: + runes[i] = Heh + case Reh: + if i == 0 { + runes[i] = Rreh + } + case RrehAbove: + runes[i] = Rreh + case Tatweel, Kasratan, Dammatan, Fathatan, Fatha, Damma, Kasra, Shadda, Sukun: + runes = analysis.DeleteRune(runes, i) + i-- + default: + if unicode.In(runes[i], unicode.Cf) { + runes = analysis.DeleteRune(runes, i) + i-- + } + } + } + return analysis.BuildTermFromRunes(runes) +} + +func NormalizerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewSoraniNormalizeFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(NormalizeName, NormalizerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ckb/sorani_normalize_test.go b/analysis/lang/ckb/sorani_normalize_test.go new file mode 100644 index 0000000..0fb0fcd --- /dev/null +++ b/analysis/lang/ckb/sorani_normalize_test.go @@ -0,0 +1,323 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ckb + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestSoraniNormalizeFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // test Y + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u064A"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06CC"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0649"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06CC"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06CC"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06CC"), + }, + }, + }, + // test K + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0643"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06A9"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06A9"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06A9"), + }, + }, + }, + // test H + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0647\u200C"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06D5"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0647\u200C\u06A9"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06D5\u06A9"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06BE"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0647"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0629"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u06D5"), + }, + }, + }, + // test final H + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0647\u0647\u0647"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0647\u0647\u06D5"), + }, + }, + }, + // test RR + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0692"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0695"), + }, + }, + }, + // test initial RR + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0631\u0631\u0631"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0695\u0631\u0631"), + }, + }, + }, + // test remove + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0640"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u064B"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u064C"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u064D"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u064E"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u064F"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0650"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0651"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0652"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u200C"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + // empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + soraniNormalizeFilter := NewSoraniNormalizeFilter() + for _, test := range tests { + actual := soraniNormalizeFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected % x, got % x", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/ckb/sorani_stemmer_filter.go b/analysis/lang/ckb/sorani_stemmer_filter.go new file mode 100644 index 0000000..533c4ba --- /dev/null +++ b/analysis/lang/ckb/sorani_stemmer_filter.go @@ -0,0 +1,151 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ckb + +import ( + "bytes" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StemmerName = "stemmer_ckb" + +type SoraniStemmerFilter struct { +} + +func NewSoraniStemmerFilter() *SoraniStemmerFilter { + return &SoraniStemmerFilter{} +} + +func (s *SoraniStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + // if not protected keyword, stem it + if !token.KeyWord { + stemmed := stem(token.Term) + token.Term = stemmed + } + } + return input +} + +func stem(input []byte) []byte { + inputLen := utf8.RuneCount(input) + + // postposition + if inputLen > 5 && bytes.HasSuffix(input, []byte("دا")) { + input = truncateRunes(input, 2) + inputLen = utf8.RuneCount(input) + } else if inputLen > 4 && bytes.HasSuffix(input, []byte("نا")) { + input = truncateRunes(input, 1) + inputLen = utf8.RuneCount(input) + } else if inputLen > 6 && bytes.HasSuffix(input, []byte("ەوە")) { + input = truncateRunes(input, 3) + inputLen = utf8.RuneCount(input) + } + + // possessive pronoun + if inputLen > 6 && + (bytes.HasSuffix(input, []byte("مان")) || + bytes.HasSuffix(input, []byte("یان")) || + bytes.HasSuffix(input, []byte("تان"))) { + input = truncateRunes(input, 3) + inputLen = utf8.RuneCount(input) + } + + // indefinite singular ezafe + if inputLen > 6 && bytes.HasSuffix(input, []byte("ێکی")) { + return truncateRunes(input, 3) + } else if inputLen > 7 && bytes.HasSuffix(input, []byte("یەکی")) { + return truncateRunes(input, 4) + } + + if inputLen > 5 && bytes.HasSuffix(input, []byte("ێک")) { + // indefinite singular + return truncateRunes(input, 2) + } else if inputLen > 6 && bytes.HasSuffix(input, []byte("یەک")) { + // indefinite singular + return truncateRunes(input, 3) + } else if inputLen > 6 && bytes.HasSuffix(input, []byte("ەکە")) { + // definite singular + return truncateRunes(input, 3) + } else if inputLen > 5 && bytes.HasSuffix(input, []byte("کە")) { + // definite singular + return truncateRunes(input, 2) + } else if inputLen > 7 && bytes.HasSuffix(input, []byte("ەکان")) { + // definite plural + return truncateRunes(input, 4) + } else if inputLen > 6 && bytes.HasSuffix(input, []byte("کان")) { + // definite plural + return truncateRunes(input, 3) + } else if inputLen > 7 && bytes.HasSuffix(input, []byte("یانی")) { + // indefinite plural ezafe + return truncateRunes(input, 4) + } else if inputLen > 6 && bytes.HasSuffix(input, []byte("انی")) { + // indefinite plural ezafe + return truncateRunes(input, 3) + } else if inputLen > 6 && bytes.HasSuffix(input, []byte("یان")) { + // indefinite plural + return truncateRunes(input, 3) + } else if inputLen > 5 && bytes.HasSuffix(input, []byte("ان")) { + // indefinite plural + return truncateRunes(input, 2) + } else if inputLen > 7 && bytes.HasSuffix(input, []byte("یانە")) { + // demonstrative plural + return truncateRunes(input, 4) + } else if inputLen > 6 && bytes.HasSuffix(input, []byte("انە")) { + // demonstrative plural + return truncateRunes(input, 3) + } else if inputLen > 5 && (bytes.HasSuffix(input, []byte("ایە")) || bytes.HasSuffix(input, []byte("ەیە"))) { + // demonstrative singular + return truncateRunes(input, 2) + } else if inputLen > 4 && bytes.HasSuffix(input, []byte("ە")) { + // demonstrative singular + return truncateRunes(input, 1) + } else if inputLen > 4 && bytes.HasSuffix(input, []byte("ی")) { + // absolute singular ezafe + return truncateRunes(input, 1) + } + return input +} + +func truncateRunes(input []byte, num int) []byte { + runes := bytes.Runes(input) + runes = runes[:len(runes)-num] + out := buildTermFromRunes(runes) + return out +} + +func buildTermFromRunes(runes []rune) []byte { + rv := make([]byte, 0, len(runes)*4) + for _, r := range runes { + runeBytes := make([]byte, utf8.RuneLen(r)) + utf8.EncodeRune(runeBytes, r) + rv = append(rv, runeBytes...) + } + return rv +} + +func StemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewSoraniStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(StemmerName, StemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ckb/sorani_stemmer_filter_test.go b/analysis/lang/ckb/sorani_stemmer_filter_test.go new file mode 100644 index 0000000..4dd9a9f --- /dev/null +++ b/analysis/lang/ckb/sorani_stemmer_filter_test.go @@ -0,0 +1,299 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ckb + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/single" +) + +func TestSoraniStemmerFilter(t *testing.T) { + + // in order to match the lucene tests + // we will test with an analyzer, not just the stemmer + analyzer := analysis.DefaultAnalyzer{ + Tokenizer: single.NewSingleTokenTokenizer(), + TokenFilters: []analysis.TokenFilter{ + NewSoraniNormalizeFilter(), + NewSoraniStemmerFilter(), + }, + } + + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { // -ek + input: []byte("پیاوێک"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 1, + Start: 0, + End: 12, + }, + }, + }, + { // -yek + input: []byte("دەرگایەک"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("دەرگا"), + Position: 1, + Start: 0, + End: 16, + }, + }, + }, + { // -aka + input: []byte("پیاوەكە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // -ka + input: []byte("دەرگاكە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("دەرگا"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // -a + input: []byte("کتاویە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("کتاوی"), + Position: 1, + Start: 0, + End: 12, + }, + }, + }, + { // -ya + input: []byte("دەرگایە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("دەرگا"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // -An + input: []byte("پیاوان"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 1, + Start: 0, + End: 12, + }, + }, + }, + { // -yAn + input: []byte("دەرگایان"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("دەرگا"), + Position: 1, + Start: 0, + End: 16, + }, + }, + }, + { // -akAn + input: []byte("پیاوەکان"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 1, + Start: 0, + End: 16, + }, + }, + }, + { // -kAn + input: []byte("دەرگاکان"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("دەرگا"), + Position: 1, + Start: 0, + End: 16, + }, + }, + }, + { // -Ana + input: []byte("پیاوانە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پیاو"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // -yAna + input: []byte("دەرگایانە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("دەرگا"), + Position: 1, + Start: 0, + End: 18, + }, + }, + }, + { // Ezafe singular + input: []byte("هۆتیلی"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هۆتیل"), + Position: 1, + Start: 0, + End: 12, + }, + }, + }, + { // Ezafe indefinite + input: []byte("هۆتیلێکی"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هۆتیل"), + Position: 1, + Start: 0, + End: 16, + }, + }, + }, + { // Ezafe plural + input: []byte("هۆتیلانی"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هۆتیل"), + Position: 1, + Start: 0, + End: 16, + }, + }, + }, + { // -awa + input: []byte("دوورەوە"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("دوور"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // -dA + input: []byte("نیوەشەودا"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("نیوەشەو"), + Position: 1, + Start: 0, + End: 18, + }, + }, + }, + { // -A + input: []byte("سۆرانا"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("سۆران"), + Position: 1, + Start: 0, + End: 12, + }, + }, + }, + { // -mAn + input: []byte("پارەمان"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پارە"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // -tAn + input: []byte("پارەتان"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پارە"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // -yAn + input: []byte("پارەیان"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("پارە"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { // empty + input: []byte(""), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + Position: 1, + Start: 0, + End: 0, + }, + }, + }, + } + + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("for input %s(% x)", test.input, test.input) + t.Errorf("\texpected:") + for _, token := range test.output { + t.Errorf("\t\t%v %s(% x)", token, token.Term, token.Term) + } + t.Errorf("\tactual:") + for _, token := range actual { + t.Errorf("\t\t%v %s(% x)", token, token.Term, token.Term) + } + } + } +} diff --git a/analysis/lang/ckb/stop_filter_ckb.go b/analysis/lang/ckb/stop_filter_ckb.go new file mode 100644 index 0000000..ed4ec8d --- /dev/null +++ b/analysis/lang/ckb/stop_filter_ckb.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ckb + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ckb/stop_words_ckb.go b/analysis/lang/ckb/stop_words_ckb.go new file mode 100644 index 0000000..87b9e81 --- /dev/null +++ b/analysis/lang/ckb/stop_words_ckb.go @@ -0,0 +1,163 @@ +package ckb + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_ckb" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var SoraniStopWords = []byte(`# set of kurdish stopwords +# note these have been normalized with our scheme (e represented with U+06D5, etc) +# constructed from: +# * Fig 5 of "Building A Test Collection For Sorani Kurdish" (Esmaili et al) +# * "Sorani Kurdish: A Reference Grammar with selected readings" (Thackston) +# * Corpus-based analysis of 77M word Sorani collection: wikipedia, news, blogs, etc + +# and +و +# which +کە +# of +ی +# made/did +کرد +# that/which +ئەوەی +# on/head +سەر +# two +دوو +# also +هەروەها +# from/that +لەو +# makes/does +دەکات +# some +چەند +# every +هەر + +# demonstratives +# that +ئەو +# this +ئەم + +# personal pronouns +# I +من +# we +ئێمە +# you +تۆ +# you +ئێوە +# he/she/it +ئەو +# they +ئەوان + +# prepositions +# to/with/by +بە +پێ +# without +بەبێ +# along with/while/during +بەدەم +# in the opinion of +بەلای +# according to +بەپێی +# before +بەرلە +# in the direction of +بەرەوی +# in front of/toward +بەرەوە +# before/in the face of +بەردەم +# without +بێ +# except for +بێجگە +# for +بۆ +# on/in +دە +تێ +# with +دەگەڵ +# after +دوای +# except for/aside from +جگە +# in/from +لە +لێ +# in front of/before/because of +لەبەر +# between/among +لەبەینی +# concerning/about +لەبابەت +# concerning +لەبارەی +# instead of +لەباتی +# beside +لەبن +# instead of +لەبرێتی +# behind +لەدەم +# with/together with +لەگەڵ +# by +لەلایەن +# within +لەناو +# between/among +لەنێو +# for the sake of +لەپێناوی +# with respect to +لەرەوی +# by means of/for +لەرێ +# for the sake of +لەرێگا +# on/on top of/according to +لەسەر +# under +لەژێر +# between/among +ناو +# between/among +نێوان +# after +پاش +# before +پێش +# like +وەک +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(SoraniStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/cs/stop_filter_cs.go b/analysis/lang/cs/stop_filter_cs.go new file mode 100644 index 0000000..52f1e93 --- /dev/null +++ b/analysis/lang/cs/stop_filter_cs.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cs + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/cs/stop_words_cs.go b/analysis/lang/cs/stop_words_cs.go new file mode 100644 index 0000000..5c3143e --- /dev/null +++ b/analysis/lang/cs/stop_words_cs.go @@ -0,0 +1,199 @@ +package cs + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_cs" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var CzechStopWords = []byte(`a +s +k +o +i +u +v +z +dnes +cz +tímto +budeš +budem +byli +jseš +můj +svým +ta +tomto +tohle +tuto +tyto +jej +zda +proč +máte +tato +kam +tohoto +kdo +kteří +mi +nám +tom +tomuto +mít +nic +proto +kterou +byla +toho +protože +asi +ho +naši +napište +re +což +tím +takže +svých +její +svými +jste +aj +tu +tedy +teto +bylo +kde +ke +pravé +ji +nad +nejsou +či +pod +téma +mezi +přes +ty +pak +vám +ani +když +však +neg +jsem +tento +článku +články +aby +jsme +před +pta +jejich +byl +ještě +až +bez +také +pouze +první +vaše +která +nás +nový +tipy +pokud +může +strana +jeho +své +jiné +zprávy +nové +není +vás +jen +podle +zde +už +být +více +bude +již +než +který +by +které +co +nebo +ten +tak +má +při +od +po +jsou +jak +další +ale +si +se +ve +to +jako +za +zpět +ze +do +pro +je +na +atd +atp +jakmile +přičemž +já +on +ona +ono +oni +ony +my +vy +jí +ji +mě +mne +jemu +tomu +těm +těmu +němu +němuž +jehož +jíž +jelikož +jež +jakož +načež +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(CzechStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/da/analyzer_da.go b/analysis/lang/da/analyzer_da.go new file mode 100644 index 0000000..8bfd554 --- /dev/null +++ b/analysis/lang/da/analyzer_da.go @@ -0,0 +1,59 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package da + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + "github.com/blevesearch/bleve/v2/registry" +) + +const AnalyzerName = "da" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopDaFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerDaFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopDaFilter, + stemmerDaFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/da/analyzer_da_test.go b/analysis/lang/da/analyzer_da_test.go new file mode 100644 index 0000000..e22f325 --- /dev/null +++ b/analysis/lang/da/analyzer_da_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package da + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestDanishAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("undersøg"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("undersøg"), + Position: 1, + Start: 0, + End: 9, + }, + }, + }, + { + input: []byte("undersøgelse"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("undersøg"), + Position: 1, + Start: 0, + End: 13, + }, + }, + }, + // stop word + { + input: []byte("på"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + } + } +} diff --git a/analysis/lang/da/stemmer_da.go b/analysis/lang/da/stemmer_da.go new file mode 100644 index 0000000..c53aa21 --- /dev/null +++ b/analysis/lang/da/stemmer_da.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package da + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/danish" +) + +const SnowballStemmerName = "stemmer_da_snowball" + +type DanishStemmerFilter struct { +} + +func NewDanishStemmerFilter() *DanishStemmerFilter { + return &DanishStemmerFilter{} +} + +func (s *DanishStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + danish.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func DanishStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewDanishStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, DanishStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/da/stop_filter_da.go b/analysis/lang/da/stop_filter_da.go new file mode 100644 index 0000000..6dd0e5c --- /dev/null +++ b/analysis/lang/da/stop_filter_da.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package da + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/da/stop_words_da.go b/analysis/lang/da/stop_words_da.go new file mode 100644 index 0000000..caaabf7 --- /dev/null +++ b/analysis/lang/da/stop_words_da.go @@ -0,0 +1,137 @@ +package da + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_da" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var DanishStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/danish/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A Danish stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + | This is a ranked list (commonest to rarest) of stopwords derived from + | a large text sample. + + +og | and +i | in +jeg | I +det | that (dem. pronoun)/it (pers. pronoun) +at | that (in front of a sentence)/to (with infinitive) +en | a/an +den | it (pers. pronoun)/that (dem. pronoun) +til | to/at/for/until/against/by/of/into, more +er | present tense of "to be" +som | who, as +på | on/upon/in/on/at/to/after/of/with/for, on +de | they +med | with/by/in, along +han | he +af | of/by/from/off/for/in/with/on, off +for | at/for/to/from/by/of/ago, in front/before, because +ikke | not +der | who/which, there/those +var | past tense of "to be" +mig | me/myself +sig | oneself/himself/herself/itself/themselves +men | but +et | a/an/one, one (number), someone/somebody/one +har | present tense of "to have" +om | round/about/for/in/a, about/around/down, if +vi | we +min | my +havde | past tense of "to have" +ham | him +hun | she +nu | now +over | over/above/across/by/beyond/past/on/about, over/past +da | then, when/as/since +fra | from/off/since, off, since +du | you +ud | out +sin | his/her/its/one's +dem | them +os | us/ourselves +op | up +man | you/one +hans | his +hvor | where +eller | or +hvad | what +skal | must/shall etc. +selv | myself/youself/herself/ourselves etc., even +her | here +alle | all/everyone/everybody etc. +vil | will (verb) +blev | past tense of "to stay/to remain/to get/to become" +kunne | could +ind | in +når | when +være | present tense of "to be" +dog | however/yet/after all +noget | something +ville | would +jo | you know/you see (adv), yes +deres | their/theirs +efter | after/behind/according to/for/by/from, later/afterwards +ned | down +skulle | should +denne | this +end | than +dette | this +mit | my/mine +også | also +under | under/beneath/below/during, below/underneath +have | have +dig | you +anden | other +hende | her +mine | my +alt | everything +meget | much/very, plenty of +sit | his, her, its, one's +sine | his, her, its, one's +vor | our +mod | against +disse | these +hvis | if +din | your/yours +nogle | some +hos | by/at +blive | be/become +mange | many +ad | by/through +bliver | present tense of "to be/to become" +hendes | her/hers +været | be +thi | for (conj) +jer | you +sådan | such, like this/like that +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(DanishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/de/analyzer_de.go b/analysis/lang/de/analyzer_de.go new file mode 100644 index 0000000..5330266 --- /dev/null +++ b/analysis/lang/de/analyzer_de.go @@ -0,0 +1,64 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + "github.com/blevesearch/bleve/v2/registry" +) + +const AnalyzerName = "de" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopDeFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + normalizeDeFilter, err := cache.TokenFilterNamed(NormalizeName) + if err != nil { + return nil, err + } + lightStemmerDeFilter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopDeFilter, + normalizeDeFilter, + lightStemmerDeFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/de/analyzer_de_test.go b/analysis/lang/de/analyzer_de_test.go new file mode 100644 index 0000000..f404ded --- /dev/null +++ b/analysis/lang/de/analyzer_de_test.go @@ -0,0 +1,155 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestGermanAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + input: []byte("Tisch"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("tisch"), + Position: 1, + Start: 0, + End: 5, + }, + }, + }, + { + input: []byte("Tische"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("tisch"), + Position: 1, + Start: 0, + End: 6, + }, + }, + }, + { + input: []byte("Tischen"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("tisch"), + Position: 1, + Start: 0, + End: 7, + }, + }, + }, + // german specials + { + input: []byte("Schaltflächen"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("schaltflach"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + { + input: []byte("Schaltflaechen"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("schaltflach"), + Position: 1, + Start: 0, + End: 14, + }, + }, + }, + // tests added by marty to increase coverage + { + input: []byte("Blechern"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("blech"), + Position: 1, + Start: 0, + End: 8, + }, + }, + }, + { + input: []byte("Klecks"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kleck"), + Position: 1, + Start: 0, + End: 6, + }, + }, + }, + { + input: []byte("Mindestens"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("mindest"), + Position: 1, + Start: 0, + End: 10, + }, + }, + }, + { + input: []byte("Kugelfest"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kugelf"), + Position: 1, + Start: 0, + End: 9, + }, + }, + }, + { + input: []byte("Baldigst"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baldig"), + Position: 1, + Start: 0, + End: 8, + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + } + } +} diff --git a/analysis/lang/de/german_normalize.go b/analysis/lang/de/german_normalize.go new file mode 100644 index 0000000..a288255 --- /dev/null +++ b/analysis/lang/de/german_normalize.go @@ -0,0 +1,98 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const NormalizeName = "normalize_de" + +const ( + N = 0 /* ordinary state */ + V = 1 /* stops 'u' from entering umlaut state */ + U = 2 /* umlaut state, allows e-deletion */ +) + +type GermanNormalizeFilter struct { +} + +func NewGermanNormalizeFilter() *GermanNormalizeFilter { + return &GermanNormalizeFilter{} +} + +func (s *GermanNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := normalize(token.Term) + token.Term = term + } + return input +} + +func normalize(input []byte) []byte { + state := N + runes := bytes.Runes(input) + for i := 0; i < len(runes); i++ { + switch runes[i] { + case 'a', 'o': + state = U + case 'u': + if state == N { + state = U + } else { + state = V + } + case 'e': + if state == U { + runes = analysis.DeleteRune(runes, i) + i-- + } + state = V + case 'i', 'q', 'y': + state = V + case 'ä': + runes[i] = 'a' + state = V + case 'ö': + runes[i] = 'o' + state = V + case 'ü': + runes[i] = 'u' + state = V + case 'ß': + runes[i] = 's' + i++ + runes = analysis.InsertRune(runes, i, 's') + state = N + default: + state = N + } + } + return analysis.BuildTermFromRunes(runes) +} + +func NormalizerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewGermanNormalizeFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(NormalizeName, NormalizerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/de/german_normalize_test.go b/analysis/lang/de/german_normalize_test.go new file mode 100644 index 0000000..81a60f5 --- /dev/null +++ b/analysis/lang/de/german_normalize_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestGermanNormalizeFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // Tests that a/o/u + e is equivalent to the umlaut form + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Schaltflächen"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Schaltflachen"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Schaltflaechen"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Schaltflachen"), + }, + }, + }, + // Tests the specific heuristic that ue is not folded after a vowel or q. + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("dauer"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("dauer"), + }, + }, + }, + // Tests german specific folding of sharp-s + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("weißbier"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("weissbier"), + }, + }, + }, + // empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + germanNormalizeFilter := NewGermanNormalizeFilter() + for _, test := range tests { + actual := germanNormalizeFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected %s(% x), got %s(% x)", test.output[0].Term, test.output[0].Term, actual[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/de/light_stemmer_de.go b/analysis/lang/de/light_stemmer_de.go new file mode 100644 index 0000000..d263de6 --- /dev/null +++ b/analysis/lang/de/light_stemmer_de.go @@ -0,0 +1,119 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const LightStemmerName = "stemmer_de_light" + +type GermanLightStemmerFilter struct { +} + +func NewGermanLightStemmerFilter() *GermanLightStemmerFilter { + return &GermanLightStemmerFilter{} +} + +func (s *GermanLightStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runes := bytes.Runes(token.Term) + runes = stem(runes) + token.Term = analysis.BuildTermFromRunes(runes) + } + return input +} + +func stem(input []rune) []rune { + + for i, r := range input { + switch r { + case 'ä', 'à', 'á', 'â': + input[i] = 'a' + case 'ö', 'ò', 'ó', 'ô': + input[i] = 'o' + case 'ï', 'ì', 'í', 'î': + input[i] = 'i' + case 'ü', 'ù', 'ú', 'û': + input[i] = 'u' + } + } + + input = step1(input) + return step2(input) +} + +func stEnding(ch rune) bool { + switch ch { + case 'b', 'd', 'f', 'g', 'h', 'k', 'l', 'm', 'n', 't': + return true + } + return false +} + +func step1(s []rune) []rune { + l := len(s) + if l > 5 && s[l-3] == 'e' && s[l-2] == 'r' && s[l-1] == 'n' { + return s[:l-3] + } + + if l > 4 && s[l-2] == 'e' { + switch s[l-1] { + case 'm', 'n', 'r', 's': + return s[:l-2] + } + } + + if l > 3 && s[l-1] == 'e' { + return s[:l-1] + } + + if l > 3 && s[l-1] == 's' && stEnding(s[l-2]) { + return s[:l-1] + } + + return s +} + +func step2(s []rune) []rune { + l := len(s) + if l > 5 && s[l-3] == 'e' && s[l-2] == 's' && s[l-1] == 't' { + return s[:l-3] + } + + if l > 4 && s[l-2] == 'e' && (s[l-1] == 'r' || s[l-1] == 'n') { + return s[:l-2] + } + + if l > 4 && s[l-2] == 's' && s[l-1] == 't' && stEnding(s[l-3]) { + return s[:l-2] + } + + return s +} + +func GermanLightStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewGermanLightStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(LightStemmerName, GermanLightStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/de/stemmer_de_snowball.go b/analysis/lang/de/stemmer_de_snowball.go new file mode 100644 index 0000000..aa61712 --- /dev/null +++ b/analysis/lang/de/stemmer_de_snowball.go @@ -0,0 +1,52 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/german" +) + +const SnowballStemmerName = "stemmer_de_snowball" + +type GermanStemmerFilter struct { +} + +func NewGermanStemmerFilter() *GermanStemmerFilter { + return &GermanStemmerFilter{} +} + +func (s *GermanStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + german.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func GermanStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewGermanStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, GermanStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/de/stemmer_de_test.go b/analysis/lang/de/stemmer_de_test.go new file mode 100644 index 0000000..5810ec4 --- /dev/null +++ b/analysis/lang/de/stemmer_de_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSnowballGermanStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abzuschrecken"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abzuschreck"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abzuwarten"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abzuwart"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("zwirnfabrik"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("zwirnfabr"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("zyniker"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("zynik"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/de/stop_filter_de.go b/analysis/lang/de/stop_filter_de.go new file mode 100644 index 0000000..235f510 --- /dev/null +++ b/analysis/lang/de/stop_filter_de.go @@ -0,0 +1,36 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package de + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/de/stop_words_de.go b/analysis/lang/de/stop_words_de.go new file mode 100644 index 0000000..d720657 --- /dev/null +++ b/analysis/lang/de/stop_words_de.go @@ -0,0 +1,321 @@ +package de + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_de" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var GermanStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/german/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A German stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + | The number of forms in this list is reduced significantly by passing it + | through the German stemmer. + + +aber | but + +alle | all +allem +allen +aller +alles + +als | than, as +also | so +am | an + dem +an | at + +ander | other +andere +anderem +anderen +anderer +anderes +anderm +andern +anderr +anders + +auch | also +auf | on +aus | out of +bei | by +bin | am +bis | until +bist | art +da | there +damit | with it +dann | then + +der | the +den +des +dem +die +das + +daß | that + +derselbe | the same +derselben +denselben +desselben +demselben +dieselbe +dieselben +dasselbe + +dazu | to that + +dein | thy +deine +deinem +deinen +deiner +deines + +denn | because + +derer | of those +dessen | of him + +dich | thee +dir | to thee +du | thou + +dies | this +diese +diesem +diesen +dieser +dieses + + +doch | (several meanings) +dort | (over) there + + +durch | through + +ein | a +eine +einem +einen +einer +eines + +einig | some +einige +einigem +einigen +einiger +einiges + +einmal | once + +er | he +ihn | him +ihm | to him + +es | it +etwas | something + +euer | your +eure +eurem +euren +eurer +eures + +für | for +gegen | towards +gewesen | p.p. of sein +hab | have +habe | have +haben | have +hat | has +hatte | had +hatten | had +hier | here +hin | there +hinter | behind + +ich | I +mich | me +mir | to me + + +ihr | you, to her +ihre +ihrem +ihren +ihrer +ihres +euch | to you + +im | in + dem +in | in +indem | while +ins | in + das +ist | is + +jede | each, every +jedem +jeden +jeder +jedes + +jene | that +jenem +jenen +jener +jenes + +jetzt | now +kann | can + +kein | no +keine +keinem +keinen +keiner +keines + +können | can +könnte | could +machen | do +man | one + +manche | some, many a +manchem +manchen +mancher +manches + +mein | my +meine +meinem +meinen +meiner +meines + +mit | with +muss | must +musste | had to +nach | to(wards) +nicht | not +nichts | nothing +noch | still, yet +nun | now +nur | only +ob | whether +oder | or +ohne | without +sehr | very + +sein | his +seine +seinem +seinen +seiner +seines + +selbst | self +sich | herself + +sie | they, she +ihnen | to them + +sind | are +so | so + +solche | such +solchem +solchen +solcher +solches + +soll | shall +sollte | should +sondern | but +sonst | else +über | over +um | about, around +und | and + +uns | us +unse +unsem +unsen +unser +unses + +unter | under +viel | much +vom | von + dem +von | from +vor | before +während | while +war | was +waren | were +warst | wast +was | what +weg | away, off +weil | because +weiter | further + +welche | which +welchem +welchen +welcher +welches + +wenn | when +werde | will +werden | will +wie | how +wieder | again +will | want +wir | we +wird | will +wirst | willst +wo | where +wollen | want +wollte | wanted +würde | would +würden | would +zu | to +zum | zu + dem +zur | zu + der +zwar | indeed +zwischen | between + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(GermanStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/el/stop_filter_el.go b/analysis/lang/el/stop_filter_el.go new file mode 100644 index 0000000..7b55bf6 --- /dev/null +++ b/analysis/lang/el/stop_filter_el.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package el + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/el/stop_words_el.go b/analysis/lang/el/stop_words_el.go new file mode 100644 index 0000000..1d2efcb --- /dev/null +++ b/analysis/lang/el/stop_words_el.go @@ -0,0 +1,105 @@ +package el + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_el" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var GreekStopWords = []byte(`# Lucene Greek Stopwords list +# Note: by default this file is used after GreekLowerCaseFilter, +# so when modifying this file use 'σ' instead of 'ς' +ο +η +το +οι +τα +του +τησ +των +τον +την +και +κι +κ +ειμαι +εισαι +ειναι +ειμαστε +ειστε +στο +στον +στη +στην +μα +αλλα +απο +για +προσ +με +σε +ωσ +παρα +αντι +κατα +μετα +θα +να +δε +δεν +μη +μην +επι +ενω +εαν +αν +τοτε +που +πωσ +ποιοσ +ποια +ποιο +ποιοι +ποιεσ +ποιων +ποιουσ +αυτοσ +αυτη +αυτο +αυτοι +αυτων +αυτουσ +αυτεσ +αυτα +εκεινοσ +εκεινη +εκεινο +εκεινοι +εκεινεσ +εκεινα +εκεινων +εκεινουσ +οπωσ +ομωσ +ισωσ +οσο +οτι +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(GreekStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/en/analyzer_en.go b/analysis/lang/en/analyzer_en.go new file mode 100644 index 0000000..b9b53a8 --- /dev/null +++ b/analysis/lang/en/analyzer_en.go @@ -0,0 +1,73 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package en implements an analyzer with reasonable defaults for processing +// English text. +// +// It strips possessive suffixes ('s), transforms tokens to lower case, +// removes stopwords from a built-in list, and applies porter stemming. +// +// The built-in stopwords list is defined in EnglishStopWords. +package en + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/token/porter" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "en" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + possEnFilter, err := cache.TokenFilterNamed(PossessiveName) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopEnFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerEnFilter, err := cache.TokenFilterNamed(porter.Name) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + possEnFilter, + toLowerFilter, + stopEnFilter, + stemmerEnFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/en/analyzer_en_test.go b/analysis/lang/en/analyzer_en_test.go new file mode 100644 index 0000000..6db7c30 --- /dev/null +++ b/analysis/lang/en/analyzer_en_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package en + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestEnglishAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("books"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("book"), + Position: 1, + Start: 0, + End: 5, + }, + }, + }, + { + input: []byte("book"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("book"), + Position: 1, + Start: 0, + End: 4, + }, + }, + }, + // stop word removal + { + input: []byte("the"), + output: analysis.TokenStream{}, + }, + // possessive removal + { + input: []byte("steven's"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("steven"), + Position: 1, + Start: 0, + End: 8, + }, + }, + }, + { + input: []byte("steven\u2019s"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("steven"), + Position: 1, + Start: 0, + End: 10, + }, + }, + }, + { + input: []byte("steven\uFF07s"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("steven"), + Position: 1, + Start: 0, + End: 10, + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + } + } +} diff --git a/analysis/lang/en/plural_stemmer.go b/analysis/lang/en/plural_stemmer.go new file mode 100644 index 0000000..7aebdc8 --- /dev/null +++ b/analysis/lang/en/plural_stemmer.go @@ -0,0 +1,177 @@ +/* + This code was ported from the Open Search Project + https://github.com/opensearch-project/OpenSearch/blob/main/modules/analysis-common/src/main/java/org/opensearch/analysis/common/EnglishPluralStemFilter.java + The algorithm itself was created by Mark Harwood + https://github.com/markharwood +*/ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package en + +import ( + "strings" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const PluralStemmerName = "stemmer_en_plural" + +type EnglishPluralStemmerFilter struct { +} + +func NewEnglishPluralStemmerFilter() *EnglishPluralStemmerFilter { + return &EnglishPluralStemmerFilter{} +} + +func (s *EnglishPluralStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + token.Term = []byte(stem(string(token.Term))) + } + + return input +} + +func EnglishPluralStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewEnglishPluralStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(PluralStemmerName, EnglishPluralStemmerFilterConstructor) + if err != nil { + panic(err) + } +} + +// ---------------------------------------------------------------------------- + +// Words ending in oes that retain the e when stemmed +var oesExceptions = []string{"shoes", "canoes", "oboes"} + +// Words ending in ches that retain the e when stemmed +var chesExceptions = []string{ + "cliches", + "avalanches", + "mustaches", + "moustaches", + "quiches", + "headaches", + "heartaches", + "porsches", + "tranches", + "caches", +} + +func stem(word string) string { + runes := []rune(strings.ToLower(word)) + + if len(runes) < 3 || runes[len(runes)-1] != 's' { + return string(runes) + } + + switch runes[len(runes)-2] { + case 'u': + fallthrough + case 's': + return string(runes) + case 'e': + // Modified ies->y logic from original s-stemmer - only work on strings > 4 + // so spies -> spy still but pies->pie. + // The original code also special-cased aies and eies for no good reason as far as I can tell. + // ( no words of consequence - eg http://www.thefreedictionary.com/words-that-end-in-aies ) + if len(runes) > 4 && runes[len(runes)-3] == 'i' { + runes[len(runes)-3] = 'y' + return string(runes[0 : len(runes)-2]) + } + + // Suffix rules to remove any dangling "e" + if len(runes) > 3 { + // xes (but >1 prefix so we can stem "boxes->box" but keep "axes->axe") + if len(runes) > 4 && runes[len(runes)-3] == 'x' { + return string(runes[0 : len(runes)-2]) + } + + // oes + if len(runes) > 3 && runes[len(runes)-3] == 'o' { + if isException(runes, oesExceptions) { + // Only remove the S + return string(runes[0 : len(runes)-1]) + } + // Remove the es + return string(runes[0 : len(runes)-2]) + } + + if len(runes) > 4 { + // shes/sses + if runes[len(runes)-4] == 's' && (runes[len(runes)-3] == 'h' || runes[len(runes)-3] == 's') { + return string(runes[0 : len(runes)-2]) + } + + // ches + if len(runes) > 4 { + if runes[len(runes)-4] == 'c' && runes[len(runes)-3] == 'h' { + if isException(runes, chesExceptions) { + // Only remove the S + return string(runes[0 : len(runes)-1]) + } + // Remove the es + return string(runes[0 : len(runes)-2]) + } + } + } + } + fallthrough + default: + return string(runes[0 : len(runes)-1]) + } +} + +func isException(word []rune, exceptions []string) bool { + for _, exception := range exceptions { + + exceptionRunes := []rune(exception) + + exceptionPos := len(exceptionRunes) - 1 + wordPos := len(word) - 1 + + matched := true + for exceptionPos >= 0 && wordPos >= 0 { + if exceptionRunes[exceptionPos] != word[wordPos] { + matched = false + break + } + exceptionPos-- + wordPos-- + } + if matched { + return true + } + } + return false +} diff --git a/analysis/lang/en/plural_stemmer_test.go b/analysis/lang/en/plural_stemmer_test.go new file mode 100644 index 0000000..b6c0028 --- /dev/null +++ b/analysis/lang/en/plural_stemmer_test.go @@ -0,0 +1,46 @@ +package en + +import "testing" + +func TestEnglishPluralStemmer(t *testing.T) { + data := []struct { + In, Out string + }{ + {"dresses", "dress"}, + {"dress", "dress"}, + {"axes", "axe"}, + {"ad", "ad"}, + {"ads", "ad"}, + {"gas", "ga"}, + {"sass", "sass"}, + {"berries", "berry"}, + {"dresses", "dress"}, + {"spies", "spy"}, + {"shoes", "shoe"}, + {"headaches", "headache"}, + {"computer", "computer"}, + {"dressing", "dressing"}, + {"clothes", "clothe"}, + {"DRESSES", "dress"}, + {"frog", "frog"}, + {"dress", "dress"}, + {"runs", "run"}, + {"pies", "pie"}, + {"foxes", "fox"}, + {"axes", "axe"}, + {"foes", "fo"}, + {"dishes", "dish"}, + {"snitches", "snitch"}, + {"cliches", "cliche"}, + {"forests", "forest"}, + {"yes", "ye"}, + } + + for _, datum := range data { + stemmed := stem(datum.In) + + if stemmed != datum.Out { + t.Errorf("expected %v but got %v", datum.Out, stemmed) + } + } +} diff --git a/analysis/lang/en/possessive_filter_en.go b/analysis/lang/en/possessive_filter_en.go new file mode 100644 index 0000000..42d51f0 --- /dev/null +++ b/analysis/lang/en/possessive_filter_en.go @@ -0,0 +1,70 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package en + +import ( + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +// PossessiveName is the name PossessiveFilter is registered as +// in the bleve registry. +const PossessiveName = "possessive_en" + +const rightSingleQuotationMark = '’' +const apostrophe = '\'' +const fullWidthApostrophe = ''' + +const apostropheChars = rightSingleQuotationMark + apostrophe + fullWidthApostrophe + +// PossessiveFilter implements a TokenFilter which +// strips the English possessive suffix ('s) from tokens. +// It handle a variety of apostrophe types, is case-insensitive +// and doesn't distinguish between possessive and contraction. +// (ie "She's So Rad" becomes "She So Rad") +type PossessiveFilter struct { +} + +func NewPossessiveFilter() *PossessiveFilter { + return &PossessiveFilter{} +} + +func (s *PossessiveFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + lastRune, lastRuneSize := utf8.DecodeLastRune(token.Term) + if lastRune == 's' || lastRune == 'S' { + nextLastRune, nextLastRuneSize := utf8.DecodeLastRune(token.Term[:len(token.Term)-lastRuneSize]) + if nextLastRune == rightSingleQuotationMark || + nextLastRune == apostrophe || + nextLastRune == fullWidthApostrophe { + token.Term = token.Term[:len(token.Term)-lastRuneSize-nextLastRuneSize] + } + } + } + return input +} + +func PossessiveFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewPossessiveFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(PossessiveName, PossessiveFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/en/possessive_filter_en_test.go b/analysis/lang/en/possessive_filter_en_test.go new file mode 100644 index 0000000..c45cfc9 --- /dev/null +++ b/analysis/lang/en/possessive_filter_en_test.go @@ -0,0 +1,142 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package en + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestEnglishPossessiveFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("marty's"), + }, + &analysis.Token{ + Term: []byte("MARTY'S"), + }, + &analysis.Token{ + Term: []byte("marty’s"), + }, + &analysis.Token{ + Term: []byte("MARTY’S"), + }, + &analysis.Token{ + Term: []byte("marty's"), + }, + &analysis.Token{ + Term: []byte("MARTY'S"), + }, + &analysis.Token{ + Term: []byte("m"), + }, + &analysis.Token{ + Term: []byte("s"), + }, + &analysis.Token{ + Term: []byte("'s"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("marty"), + }, + &analysis.Token{ + Term: []byte("MARTY"), + }, + &analysis.Token{ + Term: []byte("marty"), + }, + &analysis.Token{ + Term: []byte("MARTY"), + }, + &analysis.Token{ + Term: []byte("marty"), + }, + &analysis.Token{ + Term: []byte("MARTY"), + }, + &analysis.Token{ + Term: []byte("m"), + }, + &analysis.Token{ + Term: []byte("s"), + }, + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + cache := registry.NewCache() + stemmerFilter, err := cache.TokenFilterNamed(PossessiveName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := stemmerFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + } +} + +func BenchmarkEnglishPossessiveFilter(b *testing.B) { + + input := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("marty's"), + }, + &analysis.Token{ + Term: []byte("MARTY'S"), + }, + &analysis.Token{ + Term: []byte("marty’s"), + }, + &analysis.Token{ + Term: []byte("MARTY’S"), + }, + &analysis.Token{ + Term: []byte("marty's"), + }, + &analysis.Token{ + Term: []byte("MARTY'S"), + }, + &analysis.Token{ + Term: []byte("m"), + }, + } + + cache := registry.NewCache() + stemmerFilter, err := cache.TokenFilterNamed(PossessiveName) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stemmerFilter.Filter(input) + } + +} diff --git a/analysis/lang/en/stemmer_en_snowball.go b/analysis/lang/en/stemmer_en_snowball.go new file mode 100644 index 0000000..568a2b6 --- /dev/null +++ b/analysis/lang/en/stemmer_en_snowball.go @@ -0,0 +1,52 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package en + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/english" +) + +const SnowballStemmerName = "stemmer_en_snowball" + +type EnglishStemmerFilter struct { +} + +func NewEnglishStemmerFilter() *EnglishStemmerFilter { + return &EnglishStemmerFilter{} +} + +func (s *EnglishStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + english.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func EnglishStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewEnglishStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, EnglishStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/en/stemmer_en_test.go b/analysis/lang/en/stemmer_en_test.go new file mode 100644 index 0000000..7435d98 --- /dev/null +++ b/analysis/lang/en/stemmer_en_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package en + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSnowballEnglishStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("enjoy"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("enjoy"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("enjoyed"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("enjoy"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("enjoyable"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("enjoy"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/en/stop_filter_en.go b/analysis/lang/en/stop_filter_en.go new file mode 100644 index 0000000..0015ad6 --- /dev/null +++ b/analysis/lang/en/stop_filter_en.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package en + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/en/stop_words_en.go b/analysis/lang/en/stop_words_en.go new file mode 100644 index 0000000..d6ff496 --- /dev/null +++ b/analysis/lang/en/stop_words_en.go @@ -0,0 +1,347 @@ +package en + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_en" + +// EnglishStopWords is the built-in list of stopwords used by the "stop_en" TokenFilter. +// +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string +var EnglishStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/english/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | An English stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + | Many of the forms below are quite rare (e.g. "yourselves") but included for + | completeness. + + | PRONOUNS FORMS + | 1st person sing + +i | subject, always in upper case of course + +me | object +my | possessive adjective + | the possessive pronoun 'mine' is best suppressed, because of the + | sense of coal-mine etc. +myself | reflexive + | 1st person plural +we | subject + +| us | object + | care is required here because US = United States. It is usually + | safe to remove it if it is in lower case. +our | possessive adjective +ours | possessive pronoun +ourselves | reflexive + | second person (archaic 'thou' forms not included) +you | subject and object +your | possessive adjective +yours | possessive pronoun +yourself | reflexive (singular) +yourselves | reflexive (plural) + | third person singular +he | subject +him | object +his | possessive adjective and pronoun +himself | reflexive + +she | subject +her | object and possessive adjective +hers | possessive pronoun +herself | reflexive + +it | subject and object +its | possessive adjective +itself | reflexive + | third person plural +they | subject +them | object +their | possessive adjective +theirs | possessive pronoun +themselves | reflexive + | other forms (demonstratives, interrogatives) +what +which +who +whom +this +that +these +those + + | VERB FORMS (using F.R. Palmer's nomenclature) + | BE +am | 1st person, present +is | -s form (3rd person, present) +are | present +was | 1st person, past +were | past +be | infinitive +been | past participle +being | -ing form + | HAVE +have | simple +has | -s form +had | past +having | -ing form + | DO +do | simple +does | -s form +did | past +doing | -ing form + + | The forms below are, I believe, best omitted, because of the significant + | homonym forms: + + | He made a WILL + | old tin CAN + | merry month of MAY + | a smell of MUST + | fight the good fight with all thy MIGHT + + | would, could, should, ought might however be included + + | | AUXILIARIES + | | WILL + |will + +would + + | | SHALL + |shall + +should + + | | CAN + |can + +could + + | | MAY + |may + |might + | | MUST + |must + | | OUGHT + +ought + + | COMPOUND FORMS, increasingly encountered nowadays in 'formal' writing + | pronoun + verb + +i'm +you're +he's +she's +it's +we're +they're +i've +you've +we've +they've +i'd +you'd +he'd +she'd +we'd +they'd +i'll +you'll +he'll +she'll +we'll +they'll + + | verb + negation + +isn't +aren't +wasn't +weren't +hasn't +haven't +hadn't +doesn't +don't +didn't + + | auxiliary + negation + +won't +wouldn't +shan't +shouldn't +can't +cannot +couldn't +mustn't + + | miscellaneous forms + +let's +that's +who's +what's +here's +there's +when's +where's +why's +how's + + | rarer forms + + | daren't needn't + + | doubtful forms + + | oughtn't mightn't + + | ARTICLES +a +an +the + + | THE REST (Overlap among prepositions, conjunctions, adverbs etc is so + | high, that classification is pointless.) +and +but +if +or +because +as +until +while + +of +at +by +for +with +about +against +between +into +through +during +before +after +above +below +to +from +up +down +in +out +on +off +over +under + +again +further +then +once + +here +there +when +where +why +how + +all +any +both +each +few +more +most +other +some +such + +no +nor +not +only +own +same +so +than +too +very + + | Just for the record, the following words are among the commonest in English + + | one + | every + | least + | less + | many + | now + | ever + | never + | say + | says + | said + | also + | get + | go + | goes + | just + | made + | make + | put + | see + | seen + | whether + | like + | well + | back + | even + | still + | way + | take + | since + | another + | however + | two + | three + | four + | five + | first + | second + | new + | old + | high + | long +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(EnglishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/es/analyzer_es.go b/analysis/lang/es/analyzer_es.go new file mode 100644 index 0000000..eb4bca3 --- /dev/null +++ b/analysis/lang/es/analyzer_es.go @@ -0,0 +1,66 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package es + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "es" + +func AnalyzerConstructor(config map[string]interface{}, + cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + normalizeEsFilter, err := cache.TokenFilterNamed(NormalizeName) + if err != nil { + return nil, err + } + stopEsFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + lightStemmerEsFilter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopEsFilter, + normalizeEsFilter, + lightStemmerEsFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/es/analyzer_es_test.go b/analysis/lang/es/analyzer_es_test.go new file mode 100644 index 0000000..ad3b1f6 --- /dev/null +++ b/analysis/lang/es/analyzer_es_test.go @@ -0,0 +1,122 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package es + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSpanishAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("chicana"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chican"), + Position: 1, + Start: 0, + End: 7, + }, + }, + }, + { + input: []byte("chicano"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chican"), + Position: 1, + Start: 0, + End: 7, + }, + }, + }, + // added by marty for better coverage + { + input: []byte("yeses"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("yes"), + Position: 1, + Start: 0, + End: 5, + }, + }, + }, + { + input: []byte("jaeces"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("jaez"), + Position: 1, + Start: 0, + End: 6, + }, + }, + }, + { + input: []byte("arcos"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("arc"), + Position: 1, + Start: 0, + End: 5, + }, + }, + }, + { + input: []byte("caos"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("caos"), + Position: 1, + Start: 0, + End: 4, + }, + }, + }, + { + input: []byte("parecer"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("parecer"), + Position: 1, + Start: 0, + End: 7, + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + } + } +} diff --git a/analysis/lang/es/light_stemmer_es.go b/analysis/lang/es/light_stemmer_es.go new file mode 100644 index 0000000..22ed97f --- /dev/null +++ b/analysis/lang/es/light_stemmer_es.go @@ -0,0 +1,78 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package es + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const LightStemmerName = "stemmer_es_light" + +type SpanishLightStemmerFilter struct { +} + +func NewSpanishLightStemmerFilter() *SpanishLightStemmerFilter { + return &SpanishLightStemmerFilter{} +} + +func (s *SpanishLightStemmerFilter) Filter( + input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runes := bytes.Runes(token.Term) + runes = stem(runes) + token.Term = analysis.BuildTermFromRunes(runes) + } + return input +} + +func stem(input []rune) []rune { + l := len(input) + if l < 5 { + return input + } + + switch input[l-1] { + case 'o', 'a', 'e': + return input[:l-1] + case 's': + if input[l-2] == 'e' && input[l-3] == 's' && input[l-4] == 'e' { + return input[:l-2] + } + if input[l-2] == 'e' && input[l-3] == 'c' { + input[l-3] = 'z' + return input[:l-2] + } + if input[l-2] == 'o' || input[l-2] == 'a' || input[l-2] == 'e' { + return input[:l-2] + } + } + + return input +} + +func SpanishLightStemmerFilterConstructor(config map[string]interface{}, + cache *registry.Cache) (analysis.TokenFilter, error) { + return NewSpanishLightStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(LightStemmerName, SpanishLightStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/es/spanish_normalize.go b/analysis/lang/es/spanish_normalize.go new file mode 100644 index 0000000..8ed42a0 --- /dev/null +++ b/analysis/lang/es/spanish_normalize.go @@ -0,0 +1,70 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package es + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const NormalizeName = "normalize_es" + +type SpanishNormalizeFilter struct { +} + +func NewSpanishNormalizeFilter() *SpanishNormalizeFilter { + return &SpanishNormalizeFilter{} +} + +func (s *SpanishNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := normalize(token.Term) + token.Term = term + } + return input +} + +func normalize(input []byte) []byte { + runes := bytes.Runes(input) + for i := 0; i < len(runes); i++ { + switch runes[i] { + case 'à', 'á', 'â', 'ä': + runes[i] = 'a' + case 'ò', 'ó', 'ô', 'ö': + runes[i] = 'o' + case 'è', 'é', 'ê', 'ë': + runes[i] = 'e' + case 'ù', 'ú', 'û', 'ü': + runes[i] = 'u' + case 'ì', 'í', 'î', 'ï': + runes[i] = 'i' + } + } + + return analysis.BuildTermFromRunes(runes) +} + +func NormalizerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewSpanishNormalizeFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(NormalizeName, NormalizerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/es/spanish_normalize_test.go b/analysis/lang/es/spanish_normalize_test.go new file mode 100644 index 0000000..b2f9df5 --- /dev/null +++ b/analysis/lang/es/spanish_normalize_test.go @@ -0,0 +1,112 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package es + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestSpanishNormalizeFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Guía"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Guia"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Belcebú"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Belcebu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Limón"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Limon"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("agüero"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aguero"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("laúd"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("laud"), + }, + }, + }, + // empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + spanishNormalizeFilter := NewSpanishNormalizeFilter() + for _, test := range tests { + actual := spanishNormalizeFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected %s(% x), got %s(% x)", test.output[0].Term, test.output[0].Term, actual[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/es/stemmer_es_snowball.go b/analysis/lang/es/stemmer_es_snowball.go new file mode 100644 index 0000000..d09a11b --- /dev/null +++ b/analysis/lang/es/stemmer_es_snowball.go @@ -0,0 +1,52 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package es + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/spanish" +) + +const SnowballStemmerName = "stemmer_es_snowball" + +type SpanishStemmerFilter struct { +} + +func NewSpanishStemmerFilter() *SpanishStemmerFilter { + return &SpanishStemmerFilter{} +} + +func (s *SpanishStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + spanish.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func SpanishStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewSpanishStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, SpanishStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/es/stemmer_es_snowball_test.go b/analysis/lang/es/stemmer_es_snowball_test.go new file mode 100644 index 0000000..0531865 --- /dev/null +++ b/analysis/lang/es/stemmer_es_snowball_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package es + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSnowballSpanishStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("agresivos"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("agres"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("agresivamente"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("agres"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("agresividad"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("agres"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/es/stop_filter_es.go b/analysis/lang/es/stop_filter_es.go new file mode 100644 index 0000000..f0a6d20 --- /dev/null +++ b/analysis/lang/es/stop_filter_es.go @@ -0,0 +1,36 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package es + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, + cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/es/stop_words_es.go b/analysis/lang/es/stop_words_es.go new file mode 100644 index 0000000..4c2105b --- /dev/null +++ b/analysis/lang/es/stop_words_es.go @@ -0,0 +1,383 @@ +package es + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_es" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var SpanishStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/spanish/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A Spanish stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + + | The following is a ranked list (commonest to rarest) of stopwords + | deriving from a large sample of text. + + | Extra words have been added at the end. + +de | from, of +la | the, her +que | who, that +el | the +en | in +y | and +a | to +los | the, them +del | de + el +se | himself, from him etc +las | the, them +por | for, by, etc +un | a +para | for +con | with +no | no +una | a +su | his, her +al | a + el + | es from SER +lo | him +como | how +más | more +pero | pero +sus | su plural +le | to him, her +ya | already +o | or + | fue from SER +este | this + | ha from HABER +sí | himself etc +porque | because +esta | this + | son from SER +entre | between + | está from ESTAR +cuando | when +muy | very +sin | without +sobre | on + | ser from SER + | tiene from TENER +también | also +me | me +hasta | until +hay | there is/are +donde | where + | han from HABER +quien | whom, that + | están from ESTAR + | estado from ESTAR +desde | from +todo | all +nos | us +durante | during + | estados from ESTAR +todos | all +uno | a +les | to them +ni | nor +contra | against +otros | other + | fueron from SER +ese | that +eso | that + | había from HABER +ante | before +ellos | they +e | and (variant of y) +esto | this +mí | me +antes | before +algunos | some +qué | what? +unos | a +yo | I +otro | other +otras | other +otra | other +él | he +tanto | so much, many +esa | that +estos | these +mucho | much, many +quienes | who +nada | nothing +muchos | many +cual | who + | sea from SER +poco | few +ella | she +estar | to be + | haber from HABER +estas | these + | estaba from ESTAR + | estamos from ESTAR +algunas | some +algo | something +nosotros | we + + | other forms + +mi | me +mis | mi plural +tú | thou +te | thee +ti | thee +tu | thy +tus | tu plural +ellas | they +nosotras | we +vosotros | you +vosotras | you +os | you +mío | mine +mía | +míos | +mías | +tuyo | thine +tuya | +tuyos | +tuyas | +suyo | his, hers, theirs +suya | +suyos | +suyas | +nuestro | ours +nuestra | +nuestros | +nuestras | +vuestro | yours +vuestra | +vuestros | +vuestras | +esos | those +esas | those + + | forms of estar, to be (not including the infinitive): +estoy +estás +está +estamos +estáis +están +esté +estés +estemos +estéis +estén +estaré +estarás +estará +estaremos +estaréis +estarán +estaría +estarías +estaríamos +estaríais +estarían +estaba +estabas +estábamos +estabais +estaban +estuve +estuviste +estuvo +estuvimos +estuvisteis +estuvieron +estuviera +estuvieras +estuviéramos +estuvierais +estuvieran +estuviese +estuvieses +estuviésemos +estuvieseis +estuviesen +estando +estado +estada +estados +estadas +estad + + | forms of haber, to have (not including the infinitive): +he +has +ha +hemos +habéis +han +haya +hayas +hayamos +hayáis +hayan +habré +habrás +habrá +habremos +habréis +habrán +habría +habrías +habríamos +habríais +habrían +había +habías +habíamos +habíais +habían +hube +hubiste +hubo +hubimos +hubisteis +hubieron +hubiera +hubieras +hubiéramos +hubierais +hubieran +hubiese +hubieses +hubiésemos +hubieseis +hubiesen +habiendo +habido +habida +habidos +habidas + + | forms of ser, to be (not including the infinitive): +soy +eres +es +somos +sois +son +sea +seas +seamos +seáis +sean +seré +serás +será +seremos +seréis +serán +sería +serías +seríamos +seríais +serían +era +eras +éramos +erais +eran +fui +fuiste +fue +fuimos +fuisteis +fueron +fuera +fueras +fuéramos +fuerais +fueran +fuese +fueses +fuésemos +fueseis +fuesen +siendo +sido + | sed also means 'thirst' + + | forms of tener, to have (not including the infinitive): +tengo +tienes +tiene +tenemos +tenéis +tienen +tenga +tengas +tengamos +tengáis +tengan +tendré +tendrás +tendrá +tendremos +tendréis +tendrán +tendría +tendrías +tendríamos +tendríais +tendrían +tenía +tenías +teníamos +teníais +tenían +tuve +tuviste +tuvo +tuvimos +tuvisteis +tuvieron +tuviera +tuvieras +tuviéramos +tuvierais +tuvieran +tuviese +tuvieses +tuviésemos +tuvieseis +tuviesen +teniendo +tenido +tenida +tenidos +tenidas +tened + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(SpanishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/eu/stop_filter_eu.go b/analysis/lang/eu/stop_filter_eu.go new file mode 100644 index 0000000..12d5150 --- /dev/null +++ b/analysis/lang/eu/stop_filter_eu.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package eu + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/eu/stop_words_eu.go b/analysis/lang/eu/stop_words_eu.go new file mode 100644 index 0000000..372914c --- /dev/null +++ b/analysis/lang/eu/stop_words_eu.go @@ -0,0 +1,126 @@ +package eu + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_eu" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var BasqueStopWords = []byte(`# example set of basque stopwords +al +anitz +arabera +asko +baina +bat +batean +batek +bati +batzuei +batzuek +batzuetan +batzuk +bera +beraiek +berau +berauek +bere +berori +beroriek +beste +bezala +da +dago +dira +ditu +du +dute +edo +egin +ere +eta +eurak +ez +gainera +gu +gutxi +guzti +haiei +haiek +haietan +hainbeste +hala +han +handik +hango +hara +hari +hark +hartan +hau +hauei +hauek +hauetan +hemen +hemendik +hemengo +hi +hona +honek +honela +honetan +honi +hor +hori +horiei +horiek +horietan +horko +horra +horrek +horrela +horretan +horri +hortik +hura +izan +ni +noiz +nola +non +nondik +nongo +nor +nora +ze +zein +zen +zenbait +zenbat +zer +zergatik +ziren +zituen +zu +zuek +zuen +zuten +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(BasqueStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fa/analyzer_fa.go b/analysis/lang/fa/analyzer_fa.go new file mode 100644 index 0000000..da2cb28 --- /dev/null +++ b/analysis/lang/fa/analyzer_fa.go @@ -0,0 +1,74 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fa + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/char/zerowidthnonjoiner" + "github.com/blevesearch/bleve/v2/analysis/lang/ar" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "fa" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + zFilter, err := cache.CharFilterNamed(zerowidthnonjoiner.Name) + if err != nil { + return nil, err + } + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + normArFilter, err := cache.TokenFilterNamed(ar.NormalizeName) + if err != nil { + return nil, err + } + normFaFilter, err := cache.TokenFilterNamed(NormalizeName) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopFaFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + CharFilters: []analysis.CharFilter{ + zFilter, + }, + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + normArFilter, + normFaFilter, + stopFaFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fa/analyzer_fa_test.go b/analysis/lang/fa/analyzer_fa_test.go new file mode 100644 index 0000000..f648261 --- /dev/null +++ b/analysis/lang/fa/analyzer_fa_test.go @@ -0,0 +1,684 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fa + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestPersianAnalyzerVerbs(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // active present indicative + { + input: []byte("می‌خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active preterite indicative + { + input: []byte("خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active imperfective preterite indicative + { + input: []byte("می‌خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active future indicative + { + input: []byte("خواهد خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active present progressive indicative + { + input: []byte("دارد می‌خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active preterite progressive indicative + { + input: []byte("داشت می‌خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active perfect indicative + { + input: []byte("خورده‌است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective perfect indicative + { + input: []byte("می‌خورده‌است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active pluperfect indicative + { + input: []byte("خورده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective pluperfect indicative + { + input: []byte("می‌خورده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active preterite subjunctive + { + input: []byte("خورده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective preterite subjunctive + { + input: []byte("می‌خورده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active pluperfect subjunctive + { + input: []byte("خورده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective pluperfect subjunctive + { + input: []byte("می‌خورده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive present indicative + { + input: []byte("خورده می‌شود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive preterite indicative + { + input: []byte("خورده شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective preterite indicative + { + input: []byte("خورده می‌شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive perfect indicative + { + input: []byte("خورده شده‌است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective perfect indicative + { + input: []byte("خورده می‌شده‌است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive pluperfect indicative + { + input: []byte("خورده شده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective pluperfect indicative + { + input: []byte("خورده می‌شده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive future indicative + { + input: []byte("خورده خواهد شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive present progressive indicative + { + input: []byte("دارد خورده می‌شود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive preterite progressive indicative + { + input: []byte("داشت خورده می‌شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive present subjunctive + { + input: []byte("خورده شود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive preterite subjunctive + { + input: []byte("خورده شده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective preterite subjunctive + { + input: []byte("خورده می‌شده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive pluperfect subjunctive + { + input: []byte("خورده شده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective pluperfect subjunctive + { + input: []byte("خورده می‌شده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active present subjunctive + { + input: []byte("بخورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("بخورد"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} + +func TestPersianAnalyzerVerbsDefective(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // active present indicative + { + input: []byte("مي خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active preterite indicative + { + input: []byte("خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active imperfective preterite indicative + { + input: []byte("مي خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active future indicative + { + input: []byte("خواهد خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active present progressive indicative + { + input: []byte("دارد مي خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active preterite progressive indicative + { + input: []byte("داشت مي خورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورد"), + }, + }, + }, + // active perfect indicative + { + input: []byte("خورده است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective perfect indicative + { + input: []byte("مي خورده است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active pluperfect indicative + { + input: []byte("خورده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective pluperfect indicative + { + input: []byte("مي خورده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active preterite subjunctive + { + input: []byte("خورده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective preterite subjunctive + { + input: []byte("مي خورده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active pluperfect subjunctive + { + input: []byte("خورده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active imperfective pluperfect subjunctive + { + input: []byte("مي خورده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive present indicative + { + input: []byte("خورده مي شود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive preterite indicative + { + input: []byte("خورده شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective preterite indicative + { + input: []byte("خورده مي شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive perfect indicative + { + input: []byte("خورده شده است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective perfect indicative + { + input: []byte("خورده مي شده است"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive pluperfect indicative + { + input: []byte("خورده شده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective pluperfect indicative + { + input: []byte("خورده مي شده بود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive future indicative + { + input: []byte("خورده خواهد شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive present progressive indicative + { + input: []byte("دارد خورده مي شود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive preterite progressive indicative + { + input: []byte("داشت خورده مي شد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive present subjunctive + { + input: []byte("خورده شود"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive preterite subjunctive + { + input: []byte("خورده شده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective preterite subjunctive + { + input: []byte("خورده مي شده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive pluperfect subjunctive + { + input: []byte("خورده شده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // passive imperfective pluperfect subjunctive + { + input: []byte("خورده مي شده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + // active present subjunctive + { + input: []byte("بخورد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("بخورد"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} + +func TestPersianAnalyzerOthers(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // nouns + { + input: []byte("برگ ها"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("برگ"), + }, + }, + }, + { + input: []byte("برگ‌ها"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("برگ"), + }, + }, + }, + // non persian + { + input: []byte("English test."), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("english"), + }, + &analysis.Token{ + Term: []byte("test"), + }, + }, + }, + // others + { + input: []byte("خورده مي شده بوده باشد"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("خورده"), + }, + }, + }, + { + input: []byte("برگ‌ها"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("برگ"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/fa/persian_normalize.go b/analysis/lang/fa/persian_normalize.go new file mode 100644 index 0000000..7d73ded --- /dev/null +++ b/analysis/lang/fa/persian_normalize.go @@ -0,0 +1,80 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fa + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const NormalizeName = "normalize_fa" + +const ( + Yeh = '\u064A' + FarsiYeh = '\u06CC' + YehBarree = '\u06D2' + Keheh = '\u06A9' + Kaf = '\u0643' + HamzaAbove = '\u0654' + HehYeh = '\u06C0' + HehGoal = '\u06C1' + Heh = '\u0647' +) + +type PersianNormalizeFilter struct { +} + +func NewPersianNormalizeFilter() *PersianNormalizeFilter { + return &PersianNormalizeFilter{} +} + +func (s *PersianNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := normalize(token.Term) + token.Term = term + } + return input +} + +func normalize(input []byte) []byte { + runes := bytes.Runes(input) + for i := 0; i < len(runes); i++ { + switch runes[i] { + case FarsiYeh, YehBarree: + runes[i] = Yeh + case Keheh: + runes[i] = Kaf + case HehYeh, HehGoal: + runes[i] = Heh + case HamzaAbove: // necessary for HEH + HAMZA + runes = analysis.DeleteRune(runes, i) + i-- + } + } + return analysis.BuildTermFromRunes(runes) +} + +func NormalizerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewPersianNormalizeFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(NormalizeName, NormalizerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fa/persian_normalize_test.go b/analysis/lang/fa/persian_normalize_test.go new file mode 100644 index 0000000..4511cba --- /dev/null +++ b/analysis/lang/fa/persian_normalize_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fa + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestPersianNormalizeFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // FarsiYeh + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("های"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هاي"), + }, + }, + }, + // YehBarree + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هاے"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("هاي"), + }, + }, + }, + // Keheh + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("کشاندن"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كشاندن"), + }, + }, + }, + // HehYeh + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كتابۀ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كتابه"), + }, + }, + }, + // HehHamzaAbove + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كتابهٔ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("كتابه"), + }, + }, + }, + // HehGoal + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("زادہ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("زاده"), + }, + }, + }, + // empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + persianNormalizeFilter := NewPersianNormalizeFilter() + for _, test := range tests { + actual := persianNormalizeFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected % x, got % x", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/fa/stop_filter_fa.go b/analysis/lang/fa/stop_filter_fa.go new file mode 100644 index 0000000..bcc237b --- /dev/null +++ b/analysis/lang/fa/stop_filter_fa.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fa + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fa/stop_words_fa.go b/analysis/lang/fa/stop_words_fa.go new file mode 100644 index 0000000..4183004 --- /dev/null +++ b/analysis/lang/fa/stop_words_fa.go @@ -0,0 +1,340 @@ +package fa + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_fa" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var PersianStopWords = []byte(`# This file was created by Jacques Savoy and is distributed under the BSD license. +# See http://members.unine.ch/jacques.savoy/clef/index.html. +# Also see http://www.opensource.org/licenses/bsd-license.html +# Note: by default this file is used after normalization, so when adding entries +# to this file, use the arabic 'ي' instead of 'ی' +انان +نداشته +سراسر +خياه +ايشان +وي +تاكنون +بيشتري +دوم +پس +ناشي +وگو +يا +داشتند +سپس +هنگام +هرگز +پنج +نشان +امسال +ديگر +گروهي +شدند +چطور +ده +و +دو +نخستين +ولي +چرا +چه +وسط +ه +كدام +قابل +يك +رفت +هفت +همچنين +در +هزار +بله +بلي +شايد +اما +شناسي +گرفته +دهد +داشته +دانست +داشتن +خواهيم +ميليارد +وقتيكه +امد +خواهد +جز +اورده +شده +بلكه +خدمات +شدن +برخي +نبود +بسياري +جلوگيري +حق +كردند +نوعي +بعري +نكرده +نظير +نبايد +بوده +بودن +داد +اورد +هست +جايي +شود +دنبال +داده +بايد +سابق +هيچ +همان +انجا +كمتر +كجاست +گردد +كسي +تر +مردم +تان +دادن +بودند +سري +جدا +ندارند +مگر +يكديگر +دارد +دهند +بنابراين +هنگامي +سمت +جا +انچه +خود +دادند +زياد +دارند +اثر +بدون +بهترين +بيشتر +البته +به +براساس +بيرون +كرد +بعضي +گرفت +توي +اي +ميليون +او +جريان +تول +بر +مانند +برابر +باشيم +مدتي +گويند +اكنون +تا +تنها +جديد +چند +بي +نشده +كردن +كردم +گويد +كرده +كنيم +نمي +نزد +روي +قصد +فقط +بالاي +ديگران +اين +ديروز +توسط +سوم +ايم +دانند +سوي +استفاده +شما +كنار +داريم +ساخته +طور +امده +رفته +نخست +بيست +نزديك +طي +كنيد +از +انها +تمامي +داشت +يكي +طريق +اش +چيست +روب +نمايد +گفت +چندين +چيزي +تواند +ام +ايا +با +ان +ايد +ترين +اينكه +ديگري +راه +هايي +بروز +همچنان +پاعين +كس +حدود +مختلف +مقابل +چيز +گيرد +ندارد +ضد +همچون +سازي +شان +مورد +باره +مرسي +خويش +برخوردار +چون +خارج +شش +هنوز +تحت +ضمن +هستيم +گفته +فكر +بسيار +پيش +براي +روزهاي +انكه +نخواهد +بالا +كل +وقتي +كي +چنين +كه +گيري +نيست +است +كجا +كند +نيز +يابد +بندي +حتي +توانند +عقب +خواست +كنند +بين +تمام +همه +ما +باشند +مثل +شد +اري +باشد +اره +طبق +بعد +اگر +صورت +غير +جاي +بيش +ريزي +اند +زيرا +چگونه +بار +لطفا +مي +درباره +من +ديده +همين +گذاري +برداري +علت +گذاشته +هم +فوق +نه +ها +شوند +اباد +همواره +هر +اول +خواهند +چهار +نام +امروز +مان +هاي +قبل +كنم +سعي +تازه +را +هستند +زير +جلوي +عنوان +بود +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(PersianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fi/analyzer_fi.go b/analysis/lang/fi/analyzer_fi.go new file mode 100644 index 0000000..b2d8f1d --- /dev/null +++ b/analysis/lang/fi/analyzer_fi.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fi + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "fi" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopFiFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerFiFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopFiFilter, + stemmerFiFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fi/analyzer_fi_test.go b/analysis/lang/fi/analyzer_fi_test.go new file mode 100644 index 0000000..45aa242 --- /dev/null +++ b/analysis/lang/fi/analyzer_fi_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fi + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestFinishAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("edeltäjiinsä"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("edeltäj"), + }, + }, + }, + { + input: []byte("edeltäjistään"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("edeltäj"), + }, + }, + }, + // stop word + { + input: []byte("olla"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/fi/stemmer_fi.go b/analysis/lang/fi/stemmer_fi.go new file mode 100644 index 0000000..9b956c7 --- /dev/null +++ b/analysis/lang/fi/stemmer_fi.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fi + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/finnish" +) + +const SnowballStemmerName = "stemmer_fi_snowball" + +type FinnishStemmerFilter struct { +} + +func NewFinnishStemmerFilter() *FinnishStemmerFilter { + return &FinnishStemmerFilter{} +} + +func (s *FinnishStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + finnish.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func FinnishStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewFinnishStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, FinnishStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fi/stop_filter_fi.go b/analysis/lang/fi/stop_filter_fi.go new file mode 100644 index 0000000..55e463b --- /dev/null +++ b/analysis/lang/fi/stop_filter_fi.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fi + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fi/stop_words_fi.go b/analysis/lang/fi/stop_words_fi.go new file mode 100644 index 0000000..e7fabf9 --- /dev/null +++ b/analysis/lang/fi/stop_words_fi.go @@ -0,0 +1,124 @@ +package fi + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_fi" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var FinnishStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/finnish/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + +| forms of BE + +olla +olen +olet +on +olemme +olette +ovat +ole | negative form + +oli +olisi +olisit +olisin +olisimme +olisitte +olisivat +olit +olin +olimme +olitte +olivat +ollut +olleet + +en | negation +et +ei +emme +ette +eivät + +|Nom Gen Acc Part Iness Elat Illat Adess Ablat Allat Ess Trans +minä minun minut minua minussa minusta minuun minulla minulta minulle | I +sinä sinun sinut sinua sinussa sinusta sinuun sinulla sinulta sinulle | you +hän hänen hänet häntä hänessä hänestä häneen hänellä häneltä hänelle | he she +me meidän meidät meitä meissä meistä meihin meillä meiltä meille | we +te teidän teidät teitä teissä teistä teihin teillä teiltä teille | you +he heidän heidät heitä heissä heistä heihin heillä heiltä heille | they + +tämä tämän tätä tässä tästä tähän tallä tältä tälle tänä täksi | this +tuo tuon tuotä tuossa tuosta tuohon tuolla tuolta tuolle tuona tuoksi | that +se sen sitä siinä siitä siihen sillä siltä sille sinä siksi | it +nämä näiden näitä näissä näistä näihin näillä näiltä näille näinä näiksi | these +nuo noiden noita noissa noista noihin noilla noilta noille noina noiksi | those +ne niiden niitä niissä niistä niihin niillä niiltä niille niinä niiksi | they + +kuka kenen kenet ketä kenessä kenestä keneen kenellä keneltä kenelle kenenä keneksi| who +ketkä keiden ketkä keitä keissä keistä keihin keillä keiltä keille keinä keiksi | (pl) +mikä minkä minkä mitä missä mistä mihin millä miltä mille minä miksi | which what +mitkä | (pl) + +joka jonka jota jossa josta johon jolla jolta jolle jona joksi | who which +jotka joiden joita joissa joista joihin joilla joilta joille joina joiksi | (pl) + +| conjunctions + +että | that +ja | and +jos | if +koska | because +kuin | than +mutta | but +niin | so +sekä | and +sillä | for +tai | or +vaan | but +vai | or +vaikka | although + + +| prepositions + +kanssa | with +mukaan | according to +noin | about +poikki | across +yli | over, across + +| other + +kun | when +niin | so +nyt | now +itse | self + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(FinnishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/analyzer_fr.go b/analysis/lang/fr/analyzer_fr.go new file mode 100644 index 0000000..0808da0 --- /dev/null +++ b/analysis/lang/fr/analyzer_fr.go @@ -0,0 +1,65 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "fr" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + elisionFilter, err := cache.TokenFilterNamed(ElisionName) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopFrFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerFrFilter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + elisionFilter, + stopFrFilter, + stemmerFrFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/analyzer_fr_test.go b/analysis/lang/fr/analyzer_fr_test.go new file mode 100644 index 0000000..38f89e0 --- /dev/null +++ b/analysis/lang/fr/analyzer_fr_test.go @@ -0,0 +1,209 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestFrenchAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + input: []byte(""), + output: analysis.TokenStream{}, + }, + { + input: []byte("chien chat cheval"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chien"), + }, + &analysis.Token{ + Term: []byte("chat"), + }, + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + }, + { + input: []byte("chien CHAT CHEVAL"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chien"), + }, + &analysis.Token{ + Term: []byte("chat"), + }, + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + }, + { + input: []byte(" chien ,? + = - CHAT /: > CHEVAL"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chien"), + }, + &analysis.Token{ + Term: []byte("chat"), + }, + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + }, + { + input: []byte("chien++"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chien"), + }, + }, + }, + { + input: []byte("mot \"entreguillemet\""), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("mot"), + }, + &analysis.Token{ + Term: []byte("entreguilemet"), + }, + }, + }, + { + input: []byte("Jean-François"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("jean"), + }, + &analysis.Token{ + Term: []byte("francoi"), + }, + }, + }, + // stop words + { + input: []byte("le la chien les aux chat du des à cheval"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chien"), + }, + &analysis.Token{ + Term: []byte("chat"), + }, + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + }, + // nouns and adjectives + { + input: []byte("lances chismes habitable chiste éléments captifs"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("lanc"), + }, + &analysis.Token{ + Term: []byte("chism"), + }, + &analysis.Token{ + Term: []byte("habitabl"), + }, + &analysis.Token{ + Term: []byte("chist"), + }, + &analysis.Token{ + Term: []byte("element"), + }, + &analysis.Token{ + Term: []byte("captif"), + }, + }, + }, + // verbs + { + input: []byte("finissions souffrirent rugissante"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("finision"), + }, + &analysis.Token{ + Term: []byte("soufrirent"), + }, + &analysis.Token{ + Term: []byte("rugisant"), + }, + }, + }, + { + input: []byte("C3PO aujourd'hui oeuf ïâöûàä anticonstitutionnellement Java++ "), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("c3po"), + }, + &analysis.Token{ + Term: []byte("aujourd'hui"), + }, + &analysis.Token{ + Term: []byte("oeuf"), + }, + &analysis.Token{ + Term: []byte("ïaöuaä"), + }, + &analysis.Token{ + Term: []byte("anticonstitutionel"), + }, + &analysis.Token{ + Term: []byte("java"), + }, + }, + }, + { + input: []byte("propriétaire"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("proprietair"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/fr/articles_fr.go b/analysis/lang/fr/articles_fr.go new file mode 100644 index 0000000..540ab91 --- /dev/null +++ b/analysis/lang/fr/articles_fr.go @@ -0,0 +1,40 @@ +package fr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const ArticlesName = "articles_fr" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis + +var FrenchArticles = []byte(` +l +m +t +qu +n +s +j +d +c +jusqu +quoiqu +lorsqu +puisqu +`) + +func ArticlesTokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(FrenchArticles) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(ArticlesName, ArticlesTokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/elision_fr.go b/analysis/lang/fr/elision_fr.go new file mode 100644 index 0000000..7bd3ac9 --- /dev/null +++ b/analysis/lang/fr/elision_fr.go @@ -0,0 +1,40 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/elision" + "github.com/blevesearch/bleve/v2/registry" +) + +const ElisionName = "elision_fr" + +func ElisionFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + articlesTokenMap, err := cache.TokenMapNamed(ArticlesName) + if err != nil { + return nil, fmt.Errorf("error building elision filter: %v", err) + } + return elision.NewElisionFilter(articlesTokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(ElisionName, ElisionFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/elision_fr_test.go b/analysis/lang/fr/elision_fr_test.go new file mode 100644 index 0000000..19689ec --- /dev/null +++ b/analysis/lang/fr/elision_fr_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestFrenchElision(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("l'avion"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("avion"), + }, + }, + }, + } + + cache := registry.NewCache() + elisionFilter, err := cache.TokenFilterNamed(ElisionName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := elisionFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/fr/light_stemmer_fr.go b/analysis/lang/fr/light_stemmer_fr.go new file mode 100644 index 0000000..74f9200 --- /dev/null +++ b/analysis/lang/fr/light_stemmer_fr.go @@ -0,0 +1,309 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "bytes" + "unicode" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const LightStemmerName = "stemmer_fr_light" + +type FrenchLightStemmerFilter struct { +} + +func NewFrenchLightStemmerFilter() *FrenchLightStemmerFilter { + return &FrenchLightStemmerFilter{} +} + +func (s *FrenchLightStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runes := bytes.Runes(token.Term) + runes = stem(runes) + token.Term = analysis.BuildTermFromRunes(runes) + } + return input +} + +func stem(input []rune) []rune { + + inputLen := len(input) + + if inputLen > 5 && input[inputLen-1] == 'x' { + if input[inputLen-3] == 'a' && input[inputLen-2] == 'u' && input[inputLen-4] != 'e' { + input[inputLen-2] = 'l' + } + input = input[0 : inputLen-1] + inputLen = len(input) + } + + if inputLen > 3 && input[inputLen-1] == 'x' { + input = input[0 : inputLen-1] + inputLen = len(input) + } + + if inputLen > 3 && input[inputLen-1] == 's' { + input = input[0 : inputLen-1] + inputLen = len(input) + } + + if inputLen > 9 && analysis.RunesEndsWith(input, "issement") { + input = input[0 : inputLen-6] + inputLen = len(input) + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "issant") { + input = input[0 : inputLen-4] + inputLen = len(input) + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 6 && analysis.RunesEndsWith(input, "ement") { + input = input[0 : inputLen-4] + inputLen = len(input) + if inputLen > 3 && analysis.RunesEndsWith(input, "ive") { + input = input[0 : inputLen-1] + inputLen = len(input) + input[inputLen-1] = 'f' + } + return norm(input) + } + + if inputLen > 11 && analysis.RunesEndsWith(input, "ficatrice") { + input = input[0 : inputLen-5] + inputLen = len(input) + input[inputLen-2] = 'e' + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 10 && analysis.RunesEndsWith(input, "ficateur") { + input = input[0 : inputLen-4] + inputLen = len(input) + input[inputLen-2] = 'e' + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 9 && analysis.RunesEndsWith(input, "catrice") { + input = input[0 : inputLen-3] + inputLen = len(input) + input[inputLen-4] = 'q' + input[inputLen-3] = 'u' + input[inputLen-2] = 'e' + //s[len-1] = 'r' <-- unnecessary, already 'r'. + return norm(input) + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "cateur") { + input = input[0 : inputLen-2] + inputLen = len(input) + input[inputLen-4] = 'q' + input[inputLen-3] = 'u' + input[inputLen-2] = 'e' + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "atrice") { + input = input[0 : inputLen-4] + inputLen = len(input) + input[inputLen-2] = 'e' + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 7 && analysis.RunesEndsWith(input, "ateur") { + input = input[0 : inputLen-3] + inputLen = len(input) + input[inputLen-2] = 'e' + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 6 && analysis.RunesEndsWith(input, "trice") { + input = input[0 : inputLen-1] + inputLen = len(input) + input[inputLen-3] = 'e' + input[inputLen-2] = 'u' + input[inputLen-1] = 'r' + } + + if inputLen > 5 && analysis.RunesEndsWith(input, "ième") { + return norm(input[0 : inputLen-4]) + } + + if inputLen > 7 && analysis.RunesEndsWith(input, "teuse") { + input = input[0 : inputLen-2] + inputLen = len(input) + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 6 && analysis.RunesEndsWith(input, "teur") { + input = input[0 : inputLen-1] + inputLen = len(input) + input[inputLen-1] = 'r' + return norm(input) + } + + if inputLen > 5 && analysis.RunesEndsWith(input, "euse") { + return norm(input[0 : inputLen-2]) + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "ère") { + input = input[0 : inputLen-1] + inputLen = len(input) + input[inputLen-2] = 'e' + return norm(input) + } + + if inputLen > 7 && analysis.RunesEndsWith(input, "ive") { + input = input[0 : inputLen-1] + inputLen = len(input) + input[inputLen-1] = 'f' + return norm(input) + } + + if inputLen > 4 && + (analysis.RunesEndsWith(input, "folle") || + analysis.RunesEndsWith(input, "molle")) { + input = input[0 : inputLen-2] + inputLen = len(input) + input[inputLen-1] = 'u' + return norm(input) + } + + if inputLen > 9 && analysis.RunesEndsWith(input, "nnelle") { + return norm(input[0 : inputLen-5]) + } + + if inputLen > 9 && analysis.RunesEndsWith(input, "nnel") { + return norm(input[0 : inputLen-3]) + } + + if inputLen > 4 && analysis.RunesEndsWith(input, "ète") { + input = input[0 : inputLen-1] + inputLen = len(input) + input[inputLen-2] = 'e' + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "ique") { + input = input[0 : inputLen-4] + inputLen = len(input) + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "esse") { + return norm(input[0 : inputLen-3]) + } + + if inputLen > 7 && analysis.RunesEndsWith(input, "inage") { + return norm(input[0 : inputLen-3]) + } + + if inputLen > 9 && analysis.RunesEndsWith(input, "isation") { + input = input[0 : inputLen-7] + inputLen = len(input) + if inputLen > 5 && analysis.RunesEndsWith(input, "ual") { + input[inputLen-2] = 'e' + } + return norm(input) + } + + if inputLen > 9 && analysis.RunesEndsWith(input, "isateur") { + return norm(input[0 : inputLen-7]) + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "ation") { + return norm(input[0 : inputLen-5]) + } + + if inputLen > 8 && analysis.RunesEndsWith(input, "ition") { + return norm(input[0 : inputLen-5]) + } + + return norm(input) + +} + +func norm(input []rune) []rune { + + if len(input) > 4 { + for i := 0; i < len(input); i++ { + switch input[i] { + case 'à', 'á', 'â': + input[i] = 'a' + case 'ô': + input[i] = 'o' + case 'è', 'é', 'ê': + input[i] = 'e' + case 'ù', 'û': + input[i] = 'u' + case 'î': + input[i] = 'i' + case 'ç': + input[i] = 'c' + } + + ch := input[0] + for i := 1; i < len(input); i++ { + if input[i] == ch && unicode.IsLetter(ch) { + input = analysis.DeleteRune(input, i) + i -= 1 + } else { + ch = input[i] + } + } + } + } + + if len(input) > 4 && analysis.RunesEndsWith(input, "ie") { + input = input[0 : len(input)-2] + } + + if len(input) > 4 { + if input[len(input)-1] == 'r' { + input = input[0 : len(input)-1] + } + if input[len(input)-1] == 'e' { + input = input[0 : len(input)-1] + } + if input[len(input)-1] == 'e' { + input = input[0 : len(input)-1] + } + if input[len(input)-1] == input[len(input)-2] && unicode.IsLetter(input[len(input)-1]) { + input = input[0 : len(input)-1] + } + } + + return input +} + +func FrenchLightStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewFrenchLightStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(LightStemmerName, FrenchLightStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/light_stemmer_fr_test.go b/analysis/lang/fr/light_stemmer_fr_test.go new file mode 100644 index 0000000..a098b97 --- /dev/null +++ b/analysis/lang/fr/light_stemmer_fr_test.go @@ -0,0 +1,1015 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestFrenchLightStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chevaux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hiboux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hibou"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hibou"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hibou"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chantés"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chanter"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chante"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baronnes"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barons"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("peaux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("peau"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("peau"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("peau"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("anneaux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aneau"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("anneau"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aneau"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("neveux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("neveu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("neveu"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("neveu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("affreux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("afreu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("affreuse"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("afreu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("investissement"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("investi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("investir"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("investi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("assourdissant"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("asourdi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("assourdir"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("asourdi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("pratiquement"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("pratiqu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("pratique"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("pratiqu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administrativement"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administratif"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administratif"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administratif"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("justificatrice"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("justifi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("justificateur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("justifi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("justifier"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("justifi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("educatrice"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("eduqu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("eduquer"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("eduqu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("communicateur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("comuniqu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("communiquer"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("comuniqu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("accompagnatrice"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("acompagn"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("accompagnateur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("acompagn"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administrateur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administr"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administrer"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("administr"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("productrice"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("product"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("producteur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("product"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("acheteuse"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("achet"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("acheteur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("achet"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("planteur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("plant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("plante"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("plant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("poreuse"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("poreu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("poreux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("poreu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("plieuse"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("plieu"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bijoutière"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bijouti"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bijoutier"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bijouti"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("caissière"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("caisi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("caissier"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("caisi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abrasive"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abrasif"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abrasif"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abrasif"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("folle"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("fou"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("fou"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("fou"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("personnelle"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("person"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("personne"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("person"), + }, + }, + }, + // algo bug: too short length + // { + // input: analysis.TokenStream{ + // &analysis.Token{ + // Term: []byte("personnel"), + // }, + // }, + // output: analysis.TokenStream{ + // &analysis.Token{ + // Term: []byte("person"), + // }, + // }, + // }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("complète"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("complet"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("complet"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("complet"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aromatique"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aromat"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("faiblesse"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("faibl"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("faible"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("faibl"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("patinage"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("patin"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("patin"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("patin"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("sonorisation"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("sono"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ritualisation"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("rituel"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("rituel"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("rituel"), + }, + }, + }, + // algo bug: masked by rules above + // { + // input: analysis.TokenStream{ + // &analysis.Token{ + // Term: []byte("colonisateur"), + // }, + // }, + // output: analysis.TokenStream{ + // &analysis.Token{ + // Term: []byte("colon"), + // }, + // }, + // }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("nomination"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("nomin"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("disposition"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("dispos"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("dispose"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("dispos"), + }, + }, + }, + // SOLR-3463 : abusive compression of repeated characters in numbers + // Trailing repeated char elision : + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1234555"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1234555"), + }, + }, + }, + // Repeated char within numbers with more than 4 characters : + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("12333345"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("12333345"), + }, + }, + }, + // Short numbers weren't affected already: + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1234"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1234"), + }, + }, + }, + // Ensure behaviour is preserved for words! + // Trailing repeated char elision : + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcdeff"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcdef"), + }, + }, + }, + // Repeated char within words with more than 4 characters : + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcccddeef"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcdef"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("créées"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("cre"), + }, + }, + }, + // Combined letter and digit repetition + // 10:00pm + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("22hh00"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("22h00"), + }, + }, + }, + // bug #214 + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("propriétaire"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("proprietair"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/fr/minimal_stemmer_fr.go b/analysis/lang/fr/minimal_stemmer_fr.go new file mode 100644 index 0000000..ac6bb47 --- /dev/null +++ b/analysis/lang/fr/minimal_stemmer_fr.go @@ -0,0 +1,82 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const MinimalStemmerName = "stemmer_fr_min" + +type FrenchMinimalStemmerFilter struct { +} + +func NewFrenchMinimalStemmerFilter() *FrenchMinimalStemmerFilter { + return &FrenchMinimalStemmerFilter{} +} + +func (s *FrenchMinimalStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runes := bytes.Runes(token.Term) + runes = minstem(runes) + token.Term = analysis.BuildTermFromRunes(runes) + } + return input +} + +func minstem(input []rune) []rune { + + if len(input) < 6 { + return input + } + + if input[len(input)-1] == 'x' { + if input[len(input)-3] == 'a' && input[len(input)-2] == 'u' { + input[len(input)-2] = 'l' + } + return input[0 : len(input)-1] + } + + if input[len(input)-1] == 's' { + input = input[0 : len(input)-1] + } + if input[len(input)-1] == 'r' { + input = input[0 : len(input)-1] + } + if input[len(input)-1] == 'e' { + input = input[0 : len(input)-1] + } + if input[len(input)-1] == 'é' { + input = input[0 : len(input)-1] + } + if input[len(input)-1] == input[len(input)-2] { + input = input[0 : len(input)-1] + } + return input +} + +func FrenchMinimalStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewFrenchMinimalStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(MinimalStemmerName, FrenchMinimalStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/minimal_stemmer_fr_test.go b/analysis/lang/fr/minimal_stemmer_fr_test.go new file mode 100644 index 0000000..fa17528 --- /dev/null +++ b/analysis/lang/fr/minimal_stemmer_fr_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestFrenchMinimalStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chevaux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("cheval"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hiboux"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hibou"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chantés"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chanter"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chante"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("chant"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baronnes"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barons"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("baron"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(MinimalStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/fr/stemmer_fr_snowball.go b/analysis/lang/fr/stemmer_fr_snowball.go new file mode 100644 index 0000000..5b542a3 --- /dev/null +++ b/analysis/lang/fr/stemmer_fr_snowball.go @@ -0,0 +1,52 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/french" +) + +const SnowballStemmerName = "stemmer_fr_snowball" + +type FrenchStemmerFilter struct { +} + +func NewFrenchStemmerFilter() *FrenchStemmerFilter { + return &FrenchStemmerFilter{} +} + +func (s *FrenchStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + french.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func FrenchStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewFrenchStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, FrenchStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/stemmer_fr_snowball_test.go b/analysis/lang/fr/stemmer_fr_snowball_test.go new file mode 100644 index 0000000..69e9f26 --- /dev/null +++ b/analysis/lang/fr/stemmer_fr_snowball_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSnowballFrenchStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("antagoniste"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("antagon"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barbouillait"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barbouill"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("calculateur"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("calcul"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/fr/stop_filter_fr.go b/analysis/lang/fr/stop_filter_fr.go new file mode 100644 index 0000000..e2d4612 --- /dev/null +++ b/analysis/lang/fr/stop_filter_fr.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/fr/stop_words_fr.go b/analysis/lang/fr/stop_words_fr.go new file mode 100644 index 0000000..6a767d6 --- /dev/null +++ b/analysis/lang/fr/stop_words_fr.go @@ -0,0 +1,213 @@ +package fr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_fr" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var FrenchStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/french/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A French stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + +au | a + le +aux | a + les +avec | with +ce | this +ces | these +dans | with +de | of +des | de + les +du | de + le +elle | she +en | 'of them' etc +et | and +eux | them +il | he +je | I +la | the +le | the +leur | their +lui | him +ma | my (fem) +mais | but +me | me +même | same; as in moi-même (myself) etc +mes | me (pl) +moi | me +mon | my (masc) +ne | not +nos | our (pl) +notre | our +nous | we +on | one +ou | where +par | by +pas | not +pour | for +qu | que before vowel +que | that +qui | who +sa | his, her (fem) +se | oneself +ses | his (pl) +son | his, her (masc) +sur | on +ta | thy (fem) +te | thee +tes | thy (pl) +toi | thee +ton | thy (masc) +tu | thou +un | a +une | a +vos | your (pl) +votre | your +vous | you + + | single letter forms + +c | c' +d | d' +j | j' +l | l' +à | to, at +m | m' +n | n' +s | s' +t | t' +y | there + + | forms of être (not including the infinitive): +été +étée +étées +étés +étant +suis +es +est +sommes +êtes +sont +serai +seras +sera +serons +serez +seront +serais +serait +serions +seriez +seraient +étais +était +étions +étiez +étaient +fus +fut +fûmes +fûtes +furent +sois +soit +soyons +soyez +soient +fusse +fusses +fût +fussions +fussiez +fussent + + | forms of avoir (not including the infinitive): +ayant +eu +eue +eues +eus +ai +as +avons +avez +ont +aurai +auras +aura +aurons +aurez +auront +aurais +aurait +aurions +auriez +auraient +avais +avait +avions +aviez +avaient +eut +eûmes +eûtes +eurent +aie +aies +ait +ayons +ayez +aient +eusse +eusses +eût +eussions +eussiez +eussent + + | Later additions (from Jean-Christophe Deschamps) +ceci | this +cela | that +celà | that +cet | this +cette | this +ici | here +ils | they +les | the (pl) +leurs | their (pl) +quel | which +quels | which +quelle | which +quelles | which +sans | without +soi | oneself + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(FrenchStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ga/articles_ga.go b/analysis/lang/ga/articles_ga.go new file mode 100644 index 0000000..2ccaa36 --- /dev/null +++ b/analysis/lang/ga/articles_ga.go @@ -0,0 +1,30 @@ +package ga + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const ArticlesName = "articles_ga" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis + +var IrishArticles = []byte(` +d +m +b +`) + +func ArticlesTokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(IrishArticles) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(ArticlesName, ArticlesTokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ga/elision_ga.go b/analysis/lang/ga/elision_ga.go new file mode 100644 index 0000000..988ec50 --- /dev/null +++ b/analysis/lang/ga/elision_ga.go @@ -0,0 +1,40 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ga + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/elision" + "github.com/blevesearch/bleve/v2/registry" +) + +const ElisionName = "elision_ga" + +func ElisionFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + articlesTokenMap, err := cache.TokenMapNamed(ArticlesName) + if err != nil { + return nil, fmt.Errorf("error building elision filter: %v", err) + } + return elision.NewElisionFilter(articlesTokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(ElisionName, ElisionFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ga/elision_ga_test.go b/analysis/lang/ga/elision_ga_test.go new file mode 100644 index 0000000..809a7c3 --- /dev/null +++ b/analysis/lang/ga/elision_ga_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ga + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestFrenchElision(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("b'fhearr"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("fhearr"), + }, + }, + }, + } + + cache := registry.NewCache() + elisionFilter, err := cache.TokenFilterNamed(ElisionName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := elisionFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/ga/stop_filter_ga.go b/analysis/lang/ga/stop_filter_ga.go new file mode 100644 index 0000000..9c34924 --- /dev/null +++ b/analysis/lang/ga/stop_filter_ga.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ga + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ga/stop_words_ga.go b/analysis/lang/ga/stop_words_ga.go new file mode 100644 index 0000000..e49e671 --- /dev/null +++ b/analysis/lang/ga/stop_words_ga.go @@ -0,0 +1,137 @@ +package ga + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_ga" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var IrishStopWords = []byte(` +a +ach +ag +agus +an +aon +ar +arna +as +b' +ba +beirt +bhúr +caoga +ceathair +ceathrar +chomh +chtó +chuig +chun +cois +céad +cúig +cúigear +d' +daichead +dar +de +deich +deichniúr +den +dhá +do +don +dtí +dá +dár +dó +faoi +faoin +faoina +faoinár +fara +fiche +gach +gan +go +gur +haon +hocht +i +iad +idir +in +ina +ins +inár +is +le +leis +lena +lenár +m' +mar +mo +mé +na +nach +naoi +naonúr +ná +ní +níor +nó +nócha +ocht +ochtar +os +roimh +sa +seacht +seachtar +seachtó +seasca +seisear +siad +sibh +sinn +sna +sé +sí +tar +thar +thú +triúr +trí +trína +trínár +tríocha +tú +um +ár +é +éis +í +ó +ón +óna +ónár +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(IrishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/gl/stop_filter_gl.go b/analysis/lang/gl/stop_filter_gl.go new file mode 100644 index 0000000..5c93816 --- /dev/null +++ b/analysis/lang/gl/stop_filter_gl.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/gl/stop_words_gl.go b/analysis/lang/gl/stop_words_gl.go new file mode 100644 index 0000000..766c5ec --- /dev/null +++ b/analysis/lang/gl/stop_words_gl.go @@ -0,0 +1,188 @@ +package gl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_gl" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var GalicianStopWords = []byte(`# galican stopwords +a +aínda +alí +aquel +aquela +aquelas +aqueles +aquilo +aquí +ao +aos +as +así +á +ben +cando +che +co +coa +comigo +con +connosco +contigo +convosco +coas +cos +cun +cuns +cunha +cunhas +da +dalgunha +dalgunhas +dalgún +dalgúns +das +de +del +dela +delas +deles +desde +deste +do +dos +dun +duns +dunha +dunhas +e +el +ela +elas +eles +en +era +eran +esa +esas +ese +eses +esta +estar +estaba +está +están +este +estes +estiven +estou +eu +é +facer +foi +foron +fun +había +hai +iso +isto +la +las +lle +lles +lo +los +mais +me +meu +meus +min +miña +miñas +moi +na +nas +neste +nin +no +non +nos +nosa +nosas +noso +nosos +nós +nun +nunha +nuns +nunhas +o +os +ou +ó +ós +para +pero +pode +pois +pola +polas +polo +polos +por +que +se +senón +ser +seu +seus +sexa +sido +sobre +súa +súas +tamén +tan +te +ten +teñen +teño +ter +teu +teus +ti +tido +tiña +tiven +túa +túas +un +unha +unhas +uns +vos +vosa +vosas +voso +vosos +vós +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(GalicianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hi/analyzer_hi.go b/analysis/lang/hi/analyzer_hi.go new file mode 100644 index 0000000..4d22cf0 --- /dev/null +++ b/analysis/lang/hi/analyzer_hi.go @@ -0,0 +1,71 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hi + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/lang/in" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "hi" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + indicNormalizeFilter, err := cache.TokenFilterNamed(in.NormalizeName) + if err != nil { + return nil, err + } + hindiNormalizeFilter, err := cache.TokenFilterNamed(NormalizeName) + if err != nil { + return nil, err + } + stopHiFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerHiFilter, err := cache.TokenFilterNamed(StemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + indicNormalizeFilter, + hindiNormalizeFilter, + stopHiFilter, + stemmerHiFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hi/analyzer_hi_test.go b/analysis/lang/hi/analyzer_hi_test.go new file mode 100644 index 0000000..a86aeef --- /dev/null +++ b/analysis/lang/hi/analyzer_hi_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hi + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestHindiAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // two ways to write 'hindi' itself + { + input: []byte("हिन्दी"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("हिंद"), + Position: 1, + Start: 0, + End: 18, + }, + }, + }, + { + input: []byte("हिंदी"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("हिंद"), + Position: 1, + Start: 0, + End: 15, + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %v, got %v", test.output, actual) + } + } +} diff --git a/analysis/lang/hi/hindi_normalize.go b/analysis/lang/hi/hindi_normalize.go new file mode 100644 index 0000000..3ba5ead --- /dev/null +++ b/analysis/lang/hi/hindi_normalize.go @@ -0,0 +1,141 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hi + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const NormalizeName = "normalize_hi" + +type HindiNormalizeFilter struct { +} + +func NewHindiNormalizeFilter() *HindiNormalizeFilter { + return &HindiNormalizeFilter{} +} + +func (s *HindiNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := normalize(token.Term) + token.Term = term + } + return input +} + +func normalize(input []byte) []byte { + runes := bytes.Runes(input) + for i := 0; i < len(runes); i++ { + switch runes[i] { + // dead n -> bindu + case '\u0928': + if i+1 < len(runes) && runes[i+1] == '\u094D' { + runes[i] = '\u0902' + runes = analysis.DeleteRune(runes, i+1) + } + // candrabindu -> bindu + case '\u0901': + runes[i] = '\u0902' + // nukta deletions + case '\u093C': + runes = analysis.DeleteRune(runes, i) + i-- + case '\u0929': + runes[i] = '\u0928' + case '\u0931': + runes[i] = '\u0930' + case '\u0934': + runes[i] = '\u0933' + case '\u0958': + runes[i] = '\u0915' + case '\u0959': + runes[i] = '\u0916' + case '\u095A': + runes[i] = '\u0917' + case '\u095B': + runes[i] = '\u091C' + case '\u095C': + runes[i] = '\u0921' + case '\u095D': + runes[i] = '\u0922' + case '\u095E': + runes[i] = '\u092B' + case '\u095F': + runes[i] = '\u092F' + // zwj/zwnj -> delete + case '\u200D', '\u200C': + runes = analysis.DeleteRune(runes, i) + i-- + // virama -> delete + case '\u094D': + runes = analysis.DeleteRune(runes, i) + i-- + // chandra/short -> replace + case '\u0945', '\u0946': + runes[i] = '\u0947' + case '\u0949', '\u094A': + runes[i] = '\u094B' + case '\u090D', '\u090E': + runes[i] = '\u090F' + case '\u0911', '\u0912': + runes[i] = '\u0913' + case '\u0972': + runes[i] = '\u0905' + // long -> short ind. vowels + case '\u0906': + runes[i] = '\u0905' + case '\u0908': + runes[i] = '\u0907' + case '\u090A': + runes[i] = '\u0909' + case '\u0960': + runes[i] = '\u090B' + case '\u0961': + runes[i] = '\u090C' + case '\u0910': + runes[i] = '\u090F' + case '\u0914': + runes[i] = '\u0913' + // long -> short dep. vowels + case '\u0940': + runes[i] = '\u093F' + case '\u0942': + runes[i] = '\u0941' + case '\u0944': + runes[i] = '\u0943' + case '\u0963': + runes[i] = '\u0962' + case '\u0948': + runes[i] = '\u0947' + case '\u094C': + runes[i] = '\u094B' + } + } + return analysis.BuildTermFromRunes(runes) +} + +func NormalizerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewHindiNormalizeFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(NormalizeName, NormalizerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hi/hindi_normalize_test.go b/analysis/lang/hi/hindi_normalize_test.go new file mode 100644 index 0000000..242008d --- /dev/null +++ b/analysis/lang/hi/hindi_normalize_test.go @@ -0,0 +1,251 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hi + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestHindiNormalizeFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // basics + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अँगरेज़ी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अँगरेजी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अँग्रेज़ी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अँग्रेजी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेज़ी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंग्रेज़ी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंग्रेजी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अंगरेजि"), + }, + }, + }, + // test decompositions + // removing nukta dot + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("क़िताब"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("किताब"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("फ़र्ज़"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("फरज"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("क़र्ज़"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("करज"), + }, + }, + }, + // some other composed nukta forms + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ऱऴख़ग़ड़ढ़य़"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("रळखगडढय"), + }, + }, + }, + // removal of format (ZWJ/ZWNJ) + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("शार्‍मा"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("शारमा"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("शार्‌मा"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("शारमा"), + }, + }, + }, + // removal of chandra + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ॅॆॉॊऍऎऑऒ\u0972"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ेेोोएएओओअ"), + }, + }, + }, + // vowel shortening + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आईऊॠॡऐऔीूॄॣैौ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अइउऋऌएओिुृॢेो"), + }, + }, + }, + // empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + hindiNormalizeFilter := NewHindiNormalizeFilter() + for _, test := range tests { + actual := hindiNormalizeFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected % x, got % x", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/hi/hindi_stemmer_filter.go b/analysis/lang/hi/hindi_stemmer_filter.go new file mode 100644 index 0000000..f54cc57 --- /dev/null +++ b/analysis/lang/hi/hindi_stemmer_filter.go @@ -0,0 +1,152 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hi + +import ( + "bytes" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StemmerName = "stemmer_hi" + +type HindiStemmerFilter struct { +} + +func NewHindiStemmerFilter() *HindiStemmerFilter { + return &HindiStemmerFilter{} +} + +func (s *HindiStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + // if not protected keyword, stem it + if !token.KeyWord { + stemmed := stem(token.Term) + token.Term = stemmed + } + } + return input +} + +func stem(input []byte) []byte { + inputLen := utf8.RuneCount(input) + + // 5 + if inputLen > 6 && + (bytes.HasSuffix(input, []byte("ाएंगी")) || + bytes.HasSuffix(input, []byte("ाएंगे")) || + bytes.HasSuffix(input, []byte("ाऊंगी")) || + bytes.HasSuffix(input, []byte("ाऊंगा")) || + bytes.HasSuffix(input, []byte("ाइयाँ")) || + bytes.HasSuffix(input, []byte("ाइयों")) || + bytes.HasSuffix(input, []byte("ाइयां"))) { + return analysis.TruncateRunes(input, 5) + } + + // 4 + if inputLen > 5 && + (bytes.HasSuffix(input, []byte("ाएगी")) || + bytes.HasSuffix(input, []byte("ाएगा")) || + bytes.HasSuffix(input, []byte("ाओगी")) || + bytes.HasSuffix(input, []byte("ाओगे")) || + bytes.HasSuffix(input, []byte("एंगी")) || + bytes.HasSuffix(input, []byte("ेंगी")) || + bytes.HasSuffix(input, []byte("एंगे")) || + bytes.HasSuffix(input, []byte("ेंगे")) || + bytes.HasSuffix(input, []byte("ूंगी")) || + bytes.HasSuffix(input, []byte("ूंगा")) || + bytes.HasSuffix(input, []byte("ातीं")) || + bytes.HasSuffix(input, []byte("नाओं")) || + bytes.HasSuffix(input, []byte("नाएं")) || + bytes.HasSuffix(input, []byte("ताओं")) || + bytes.HasSuffix(input, []byte("ताएं")) || + bytes.HasSuffix(input, []byte("ियाँ")) || + bytes.HasSuffix(input, []byte("ियों")) || + bytes.HasSuffix(input, []byte("ियां"))) { + return analysis.TruncateRunes(input, 4) + } + + // 3 + if inputLen > 4 && + (bytes.HasSuffix(input, []byte("ाकर")) || + bytes.HasSuffix(input, []byte("ाइए")) || + bytes.HasSuffix(input, []byte("ाईं")) || + bytes.HasSuffix(input, []byte("ाया")) || + bytes.HasSuffix(input, []byte("ेगी")) || + bytes.HasSuffix(input, []byte("ेगा")) || + bytes.HasSuffix(input, []byte("ोगी")) || + bytes.HasSuffix(input, []byte("ोगे")) || + bytes.HasSuffix(input, []byte("ाने")) || + bytes.HasSuffix(input, []byte("ाना")) || + bytes.HasSuffix(input, []byte("ाते")) || + bytes.HasSuffix(input, []byte("ाती")) || + bytes.HasSuffix(input, []byte("ाता")) || + bytes.HasSuffix(input, []byte("तीं")) || + bytes.HasSuffix(input, []byte("ाओं")) || + bytes.HasSuffix(input, []byte("ाएं")) || + bytes.HasSuffix(input, []byte("ुओं")) || + bytes.HasSuffix(input, []byte("ुएं")) || + bytes.HasSuffix(input, []byte("ुआं"))) { + return analysis.TruncateRunes(input, 3) + } + + // 2 + if inputLen > 3 && + (bytes.HasSuffix(input, []byte("कर")) || + bytes.HasSuffix(input, []byte("ाओ")) || + bytes.HasSuffix(input, []byte("िए")) || + bytes.HasSuffix(input, []byte("ाई")) || + bytes.HasSuffix(input, []byte("ाए")) || + bytes.HasSuffix(input, []byte("ने")) || + bytes.HasSuffix(input, []byte("नी")) || + bytes.HasSuffix(input, []byte("ना")) || + bytes.HasSuffix(input, []byte("ते")) || + bytes.HasSuffix(input, []byte("ीं")) || + bytes.HasSuffix(input, []byte("ती")) || + bytes.HasSuffix(input, []byte("ता")) || + bytes.HasSuffix(input, []byte("ाँ")) || + bytes.HasSuffix(input, []byte("ां")) || + bytes.HasSuffix(input, []byte("ों")) || + bytes.HasSuffix(input, []byte("ें"))) { + return analysis.TruncateRunes(input, 2) + } + + // 1 + if inputLen > 2 && + (bytes.HasSuffix(input, []byte("ो")) || + bytes.HasSuffix(input, []byte("े")) || + bytes.HasSuffix(input, []byte("ू")) || + bytes.HasSuffix(input, []byte("ु")) || + bytes.HasSuffix(input, []byte("ी")) || + bytes.HasSuffix(input, []byte("ि")) || + bytes.HasSuffix(input, []byte("ा"))) { + return analysis.TruncateRunes(input, 1) + } + + return input +} + +func StemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewHindiStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(StemmerName, StemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hi/hindi_stemmer_filter_test.go b/analysis/lang/hi/hindi_stemmer_filter_test.go new file mode 100644 index 0000000..bf5d92a --- /dev/null +++ b/analysis/lang/hi/hindi_stemmer_filter_test.go @@ -0,0 +1,308 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hi + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestHindiStemmerFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // masc noun inflections + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडका"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडक"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडके"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडक"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडकों"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडक"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("गुरु"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("गुर"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("गुरुओं"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("गुर"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("दोस्त"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("दोस्त"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("दोस्तों"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("दोस्त"), + }, + }, + }, + // feminine noun inflections + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडकी"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडक"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडकियों"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("लडक"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("किताब"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("किताब"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("किताबें"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("किताब"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("किताबों"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("किताब"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आध्यापीका"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आध्यापीक"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आध्यापीकाएं"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आध्यापीक"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आध्यापीकाओं"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आध्यापीक"), + }, + }, + }, + // some verb forms + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खाना"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खा"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खाता"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खा"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खाती"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खा"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खा"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("खा"), + }, + }, + }, + // exceptions + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("कठिनाइयां"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("कठिन"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("कठिन"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("कठिन"), + }, + }, + }, + // empty + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + hindiStemmerFilter := NewHindiStemmerFilter() + for _, test := range tests { + actual := hindiStemmerFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected % x, got % x", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/hi/stop_filter_hi.go b/analysis/lang/hi/stop_filter_hi.go new file mode 100644 index 0000000..8122b4f --- /dev/null +++ b/analysis/lang/hi/stop_filter_hi.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hi + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hi/stop_words_hi.go b/analysis/lang/hi/stop_words_hi.go new file mode 100644 index 0000000..844c774 --- /dev/null +++ b/analysis/lang/hi/stop_words_hi.go @@ -0,0 +1,262 @@ +package hi + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_hi" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var HindiStopWords = []byte(`# Also see http://www.opensource.org/licenses/bsd-license.html +# See http://members.unine.ch/jacques.savoy/clef/index.html. +# This file was created by Jacques Savoy and is distributed under the BSD license. +# Note: by default this file also contains forms normalized by HindiNormalizer +# for spelling variation (see section below), such that it can be used whether or +# not you enable that feature. When adding additional entries to this list, +# please add the normalized form as well. +अंदर +अत +अपना +अपनी +अपने +अभी +आदि +आप +इत्यादि +इन +इनका +इन्हीं +इन्हें +इन्हों +इस +इसका +इसकी +इसके +इसमें +इसी +इसे +उन +उनका +उनकी +उनके +उनको +उन्हीं +उन्हें +उन्हों +उस +उसके +उसी +उसे +एक +एवं +एस +ऐसे +और +कई +कर +करता +करते +करना +करने +करें +कहते +कहा +का +काफ़ी +कि +कितना +किन्हें +किन्हों +किया +किर +किस +किसी +किसे +की +कुछ +कुल +के +को +कोई +कौन +कौनसा +गया +घर +जब +जहाँ +जा +जितना +जिन +जिन्हें +जिन्हों +जिस +जिसे +जीधर +जैसा +जैसे +जो +तक +तब +तरह +तिन +तिन्हें +तिन्हों +तिस +तिसे +तो +था +थी +थे +दबारा +दिया +दुसरा +दूसरे +दो +द्वारा +न +नहीं +ना +निहायत +नीचे +ने +पर +पर +पहले +पूरा +पे +फिर +बनी +बही +बहुत +बाद +बाला +बिलकुल +भी +भीतर +मगर +मानो +मे +में +यदि +यह +यहाँ +यही +या +यिह +ये +रखें +रहा +रहे +ऱ्वासा +लिए +लिये +लेकिन +व +वर्ग +वह +वह +वहाँ +वहीं +वाले +वुह +वे +वग़ैरह +संग +सकता +सकते +सबसे +सभी +साथ +साबुत +साभ +सारा +से +सो +ही +हुआ +हुई +हुए +है +हैं +हो +होता +होती +होते +होना +होने +# additional normalized forms of the above +अपनि +जेसे +होति +सभि +तिंहों +इंहों +दवारा +इसि +किंहें +थि +उंहों +ओर +जिंहें +वहिं +अभि +बनि +हि +उंहिं +उंहें +हें +वगेरह +एसे +रवासा +कोन +निचे +काफि +उसि +पुरा +भितर +हे +बहि +वहां +कोइ +यहां +जिंहों +तिंहें +किसि +कइ +यहि +इंहिं +जिधर +इंहें +अदि +इतयादि +हुइ +कोनसा +इसकि +दुसरे +जहां +अप +किंहों +उनकि +भि +वरग +हुअ +जेसा +नहिं +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(HindiStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hr/analyzer_hr.go b/analysis/lang/hr/analyzer_hr.go new file mode 100644 index 0000000..7deaeda --- /dev/null +++ b/analysis/lang/hr/analyzer_hr.go @@ -0,0 +1,67 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +// Originated from: http://nlp.ffzg.hr/resources/tools/stemmer-for-croatian/ + +const AnalyzerName = "hr" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + suffixFilter, err := cache.TokenFilterNamed(SuffixTransformationFilterName) + if err != nil { + return nil, err + } + stemmerFilter, err := cache.TokenFilterNamed(StemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopFilter, + suffixFilter, + stemmerFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hr/analyzer_hr_test.go b/analysis/lang/hr/analyzer_hr_test.go new file mode 100644 index 0000000..e1ab35a --- /dev/null +++ b/analysis/lang/hr/analyzer_hr_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestCroatianAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("Hrvatska"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hrvatsk"), + }, + }, + }, + { + input: []byte("Hrvatski"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("hrvatsk"), + }, + }, + }, + // uppercase letters + { + input: []byte("KOMARAC"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("komarc"), + }, + }, + }, + // vowelR + { + input: []byte("crvi"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("crv"), + }, + }, + }, + // stop word + { + input: []byte("biti"), + output: analysis.TokenStream{}, + }, + // suffix transformation + { + input: []byte("zaključcima"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("zaključk"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/hr/stemmer_hr.go b/analysis/lang/hr/stemmer_hr.go new file mode 100644 index 0000000..e6e41f5 --- /dev/null +++ b/analysis/lang/hr/stemmer_hr.go @@ -0,0 +1,156 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hr + +import ( + "regexp" + "strings" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StemmerName = "stemmer_hr" + +// These regular expressions rules originated from: +// http://nlp.ffzg.hr/resources/tools/stemmer-for-croatian/ + +var stemmingRules = []*regexp.Regexp{ + regexp.MustCompile(`^(.+(s|š)k)(ijima|ijega|ijemu|ijem|ijim|ijih|ijoj|ijeg|iji|ije|ija|oga|ome|omu|ima|og|om|im|ih|oj|i|e|o|a|u)$`), + regexp.MustCompile(`^(.+(s|š)tv)(ima|om|o|a|u)$`), + regexp.MustCompile(`^(.+(t|m|p|r|g)anij)(ama|ima|om|a|u|e|i|)$`), + regexp.MustCompile(`^(.+an)(inom|ina|inu|ine|ima|in|om|u|i|a|e|)$`), + regexp.MustCompile(`^(.+in)(ima|ama|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+on)(ovima|ova|ove|ovi|ima|om|a|e|i|u|)$`), + regexp.MustCompile(`^(.+n)(ijima|ijega|ijemu|ijeg|ijem|ijim|ijih|ijoj|iji|ije|ija|iju|ima|ome|omu|oga|oj|om|ih|im|og|o|e|a|u|i|)$`), + regexp.MustCompile(`^(.+(a|e|u)ć)(oga|ome|omu|ega|emu|ima|oj|ih|om|eg|em|og|uh|im|e|a)$`), + regexp.MustCompile(`^(.+ugov)(ima|i|e|a)$`), + regexp.MustCompile(`^(.+ug)(ama|om|a|e|i|u|o)$`), + regexp.MustCompile(`^(.+log)(ama|om|a|u|e|)$`), + regexp.MustCompile(`^(.+[^eo]g)(ovima|ama|ovi|ove|ova|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+(rrar|ott|ss|ll)i)(jem|ja|ju|o|)$`), + regexp.MustCompile(`^(.+uj)(ući|emo|ete|mo|em|eš|e|u|)$`), + regexp.MustCompile(`^(.+(c|č|ć|đ|l|r)aj)(evima|evi|eva|eve|ama|ima|em|a|e|i|u|)$`), + regexp.MustCompile(`^(.+(b|c|d|l|n|m|ž|g|f|p|r|s|t|z)ij)(ima|ama|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+[^z]nal)(ima|ama|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+ijal)(ima|ama|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+ozil)(ima|om|a|e|u|i|)$`), + regexp.MustCompile(`^(.+olov)(ima|i|a|e)$`), + regexp.MustCompile(`^(.+ol)(ima|om|a|u|e|i|)$`), + regexp.MustCompile(`^(.+lem)(ama|ima|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+ram)(ama|om|a|e|i|u|o)$`), + regexp.MustCompile(`^(.+(a|d|e|o)r)(ama|ima|om|u|a|e|i|)$`), + regexp.MustCompile(`^(.+(e|i)s)(ima|om|e|a|u)$`), + regexp.MustCompile(`^(.+(t|n|j|k|j|t|b|g|v)aš)(ama|ima|om|em|a|u|i|e|)$`), + regexp.MustCompile(`^(.+(e|i)š)(ima|ama|om|em|i|e|a|u|)$`), + regexp.MustCompile(`^(.+ikat)(ima|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+lat)(ima|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+et)(ama|ima|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+(e|i|k|o)st)(ima|ama|om|a|e|i|u|o|)$`), + regexp.MustCompile(`^(.+išt)(ima|em|a|e|u)$`), + regexp.MustCompile(`^(.+ova)(smo|ste|hu|ti|še|li|la|le|lo|t|h|o)$`), + regexp.MustCompile(`^(.+(a|e|i)v)(ijemu|ijima|ijega|ijeg|ijem|ijim|ijih|ijoj|oga|ome|omu|ima|ama|iji|ije|ija|iju|im|ih|oj|om|og|i|a|u|e|o|)$`), + regexp.MustCompile(`^(.+[^dkml]ov)(ijemu|ijima|ijega|ijeg|ijem|ijim|ijih|ijoj|oga|ome|omu|ima|iji|ije|ija|iju|im|ih|oj|om|og|i|a|u|e|o|)$`), + regexp.MustCompile(`^(.+(m|l)ov)(ima|om|a|u|e|i|)$`), + regexp.MustCompile(`^(.+el)(ijemu|ijima|ijega|ijeg|ijem|ijim|ijih|ijoj|oga|ome|omu|ima|iji|ije|ija|iju|im|ih|oj|om|og|i|a|u|e|o|)$`), + regexp.MustCompile(`^(.+(a|e|š)nj)(ijemu|ijima|ijega|ijeg|ijem|ijim|ijih|ijoj|oga|ome|omu|ima|iji|ije|ija|iju|ega|emu|eg|em|im|ih|oj|om|og|a|e|i|o|u)$`), + regexp.MustCompile(`^(.+čin)(ama|ome|omu|oga|ima|og|om|im|ih|oj|a|u|i|o|e|)$`), + regexp.MustCompile(`^(.+roši)(vši|smo|ste|še|mo|te|ti|li|la|lo|le|m|š|t|h|o)$`), + regexp.MustCompile(`^(.+oš)(ijemu|ijima|ijega|ijeg|ijem|ijim|ijih|ijoj|oga|ome|omu|ima|iji|ije|ija|iju|im|ih|oj|om|og|i|a|u|e|)$`), + regexp.MustCompile(`^(.+(e|o)vit)(ijima|ijega|ijemu|ijem|ijim|ijih|ijoj|ijeg|iji|ije|ija|oga|ome|omu|ima|og|om|im|ih|oj|i|e|o|a|u|)$`), + regexp.MustCompile(`^(.+ast)(ijima|ijega|ijemu|ijem|ijim|ijih|ijoj|ijeg|iji|ije|ija|oga|ome|omu|ima|og|om|im|ih|oj|i|e|o|a|u|)$`), + regexp.MustCompile(`^(.+k)(ijemu|ijima|ijega|ijeg|ijem|ijim|ijih|ijoj|oga|ome|omu|ima|iji|ije|ija|iju|im|ih|oj|om|og|i|a|u|e|o|)$`), + regexp.MustCompile(`^(.+(e|a|i|u)va)(jući|smo|ste|jmo|jte|ju|la|le|li|lo|mo|na|ne|ni|no|te|ti|še|hu|h|j|m|n|o|t|v|š|)$`), + regexp.MustCompile(`^(.+ir)(ujemo|ujete|ujući|ajući|ivat|ujem|uješ|ujmo|ujte|avši|asmo|aste|ati|amo|ate|aju|aše|ahu|ala|alo|ali|ale|uje|uju|uj|al|an|am|aš|at|ah|ao)$`), + regexp.MustCompile(`^(.+ač)(ismo|iste|iti|imo|ite|iše|eći|ila|ilo|ili|ile|ena|eno|eni|ene|io|im|iš|it|ih|en|i|e)$`), + regexp.MustCompile(`^(.+ača)(vši|smo|ste|smo|ste|hu|ti|mo|te|še|la|lo|li|le|ju|na|no|ni|ne|o|m|š|t|h|n)$`), + regexp.MustCompile(`^(.+n)(uvši|usmo|uste|ući|imo|ite|emo|ete|ula|ulo|ule|uli|uto|uti|uta|em|eš|uo|ut|e|u|i)$`), + regexp.MustCompile(`^(.+ni)(vši|smo|ste|ti|mo|te|mo|te|la|lo|le|li|m|š|o)$`), + regexp.MustCompile(`^(.+((a|r|i|p|e|u)st|[^o]g|ik|uc|oj|aj|lj|ak|ck|čk|šk|uk|nj|im|ar|at|et|št|it|ot|ut|zn|zv)a)(jući|vši|smo|ste|jmo|jte|jem|mo|te|je|ju|ti|še|hu|la|li|le|lo|na|no|ni|ne|t|h|o|j|n|m|š)$`), + regexp.MustCompile(`^(.+ur)(ajući|asmo|aste|ajmo|ajte|amo|ate|aju|ati|aše|ahu|ala|ali|ale|alo|ana|ano|ani|ane|al|at|ah|ao|aj|an|am|aš)$`), + regexp.MustCompile(`^(.+(a|i|o)staj)(asmo|aste|ahu|ati|emo|ete|aše|ali|ući|ala|alo|ale|mo|ao|em|eš|at|ah|te|e|u|)$`), + regexp.MustCompile(`^(.+(b|c|č|ć|d|e|f|g|j|k|n|r|t|u|v)a)(lama|lima|lom|lu|li|la|le|lo|l)$`), + regexp.MustCompile(`^(.+(t|č|j|ž|š)aj)(evima|evi|eva|eve|ama|ima|em|a|e|i|u|)$`), + regexp.MustCompile(`^(.+([^o]m|ič|nč|uč|b|c|ć|d|đ|h|j|k|l|n|p|r|s|š|v|z|ž)a)(jući|vši|smo|ste|jmo|jte|mo|te|ju|ti|še|hu|la|li|le|lo|na|no|ni|ne|t|h|o|j|n|m|š)$`), + regexp.MustCompile(`^(.+(a|i|o)sta)(dosmo|doste|doše|nemo|demo|nete|dete|nimo|nite|nila|vši|nem|dem|neš|deš|doh|de|ti|ne|nu|du|la|li|lo|le|t|o)$`), + regexp.MustCompile(`^(.+ta)(smo|ste|jmo|jte|vši|ti|mo|te|ju|še|la|lo|le|li|na|no|ni|ne|n|j|o|m|š|t|h)$`), + regexp.MustCompile(`^(.+inj)(asmo|aste|ati|emo|ete|ali|ala|alo|ale|aše|ahu|em|eš|at|ah|ao)$`), + regexp.MustCompile(`^(.+as)(temo|tete|timo|tite|tući|tem|teš|tao|te|li|ti|la|lo|le)$`), + regexp.MustCompile(`^(.+(elj|ulj|tit|ac|ič|od|oj|et|av|ov)i)(vši|eći|smo|ste|še|mo|te|ti|li|la|lo|le|m|š|t|h|o)$`), + regexp.MustCompile(`^(.+(tit|jeb|ar|ed|uš|ič)i)(jemo|jete|jem|ješ|smo|ste|jmo|jte|vši|mo|še|te|ti|ju|je|la|lo|li|le|t|m|š|h|j|o)$`), + regexp.MustCompile(`^(.+(b|č|d|l|m|p|r|s|š|ž)i)(jemo|jete|jem|ješ|smo|ste|jmo|jte|vši|mo|lu|še|te|ti|ju|je|la|lo|li|le|t|m|š|h|j|o)$`), + regexp.MustCompile(`^(.+luč)(ujete|ujući|ujemo|ujem|uješ|ismo|iste|ujmo|ujte|uje|uju|iše|iti|imo|ite|ila|ilo|ili|ile|ena|eno|eni|ene|uj|io|en|im|iš|it|ih|e|i)$`), + regexp.MustCompile(`^(.+jeti)(smo|ste|še|mo|te|ti|li|la|lo|le|m|š|t|h|o)$`), + regexp.MustCompile(`^(.+e)(lama|lima|lom|lu|li|la|le|lo|l)$`), + regexp.MustCompile(`^(.+i)(lama|lima|lom|lu|li|la|le|lo|l)$`), + regexp.MustCompile(`^(.+at)(ijega|ijemu|ijima|ijeg|ijem|ijih|ijim|ima|oga|ome|omu|iji|ije|ija|iju|oj|og|om|im|ih|a|u|i|e|o|)$`), + regexp.MustCompile(`^(.+et)(avši|ući|emo|imo|em|eš|e|u|i)$`), + regexp.MustCompile(`^(.+)(ajući|alima|alom|avši|asmo|aste|ajmo|ajte|ivši|amo|ate|aju|ati|aše|ahu|ali|ala|ale|alo|ana|ano|ani|ane|am|aš|at|ah|ao|aj|an)$`), + regexp.MustCompile(`^(.+)(anje|enje|anja|enja|enom|enoj|enog|enim|enih|anom|anoj|anog|anim|anih|eno|ovi|ova|oga|ima|ove|enu|anu|ena|ama)$`), + regexp.MustCompile(`^(.+)(nijega|nijemu|nijima|nijeg|nijem|nijim|nijih|nima|niji|nije|nija|niju|noj|nom|nog|nim|nih|an|na|nu|ni|ne|no)$`), + regexp.MustCompile(`^(.+)(om|og|im|ih|em|oj|an|u|o|i|e|a)$`), +} + +var highlightVowelRRegex = regexp.MustCompile(`(^|[^aeiou])r($|[^aeiou])`) + +func highlightVowelR(term string) string { + return highlightVowelRRegex.ReplaceAllString(term, `${1}R${2}`) +} + +func hasVowel(term string) bool { + term = highlightVowelR(term) + return strings.ContainsAny(term, "aeiouR") +} + +func stem(term string) string { + for _, rule := range stemmingRules { + results := rule.FindStringSubmatch(term) + if len(results) == 0 { + continue + } + + root := results[1] + if hasVowel(root) && root != "" { + return root + } + } + + return term +} + +type CroatianStemmerFilter struct{} + +func NewCroatianStemmerFilter() *CroatianStemmerFilter { + return &CroatianStemmerFilter{} +} + +func (s *CroatianStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + token.Term = []byte(stem(string(token.Term))) + } + + return input +} + +func CroatianStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewCroatianStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(StemmerName, CroatianStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hr/stop_filter_hr.go b/analysis/lang/hr/stop_filter_hr.go new file mode 100644 index 0000000..c79015f --- /dev/null +++ b/analysis/lang/hr/stop_filter_hr.go @@ -0,0 +1,36 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hr/stop_words_hr.go b/analysis/lang/hr/stop_words_hr.go new file mode 100644 index 0000000..3bd3845 --- /dev/null +++ b/analysis/lang/hr/stop_words_hr.go @@ -0,0 +1,111 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_hr" + +var CroatianStopWords = []byte(`biti +jesam +budem +sam +jesi +budeš +si +jesmo +budemo +smo +jeste +budete +ste +jesu +budu +su +bih +bijah +bjeh +bijaše +bi +bje +bješe +bijasmo +bismo +bjesmo +bijaste +biste +bjeste +bijahu +biste +bjeste +bijahu +bi +biše +bjehu +bješe +bio +bili +budimo +budite +bila +bilo +bile +ću +ćeš +će +ćemo +ćete +želim +želiš +želi +želimo +želite +žele +moram +moraš +mora +moramo +morate +moraju +trebam +trebaš +treba +trebamo +trebate +trebaju +mogu +možeš +može +možemo +možete +za +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(CroatianStopWords) + + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hr/suffix_transformation_hr.go b/analysis/lang/hr/suffix_transformation_hr.go new file mode 100644 index 0000000..3223ee4 --- /dev/null +++ b/analysis/lang/hr/suffix_transformation_hr.go @@ -0,0 +1,189 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hr + +import ( + "strings" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const SuffixTransformationFilterName = "hr_suffix_transformation_filter" + +var SuffixTransformations = map[string]string{ + "lozi": "loga", + "lozima": "loga", + "pjesi": "pjeh", + "pjesima": "pjeh", + "vojci": "vojka", + "bojci": "bojka", + "jaci": "jak", + "jacima": "jak", + "čajan": "čajni", + "ijeran": "ijerni", + "laran": "larni", + "ijesan": "ijesni", + "anjac": "anjca", + "ajac": "ajca", + "ajaca": "ajca", + "ljaca": "ljca", + "ljac": "ljca", + "ejac": "ejca", + "ejaca": "ejca", + "ojac": "ojca", + "ojaca": "ojca", + "ajaka": "ajka", + "ojaka": "ojka", + "šaca": "šca", + "šac": "šca", + "inzima": "ing", + "inzi": "ing", + "tvenici": "tvenik", + "tetici": "tetika", + "teticima": "tetika", + "nstava": "nstva", + "nicima": "nik", + "ticima": "tik", + "zicima": "zik", + "snici": "snik", + "kuse": "kusi", + "kusan": "kusni", + "kustava": "kustva", + "dušan": "dušni", + "antan": "antni", + "bilan": "bilni", + "tilan": "tilni", + "avilan": "avilni", + "silan": "silni", + "gilan": "gilni", + "rilan": "rilni", + "nilan": "nilni", + "alan": "alni", + "ozan": "ozni", + "rave": "ravi", + "stavan": "stavni", + "pravan": "pravni", + "tivan": "tivni", + "sivan": "sivni", + "atan": "atni", + "cenata": "centa", + "denata": "denta", + "genata": "genta", + "lenata": "lenta", + "menata": "menta", + "jenata": "jenta", + "venata": "venta", + "tetan": "tetni", + "pletan": "pletni", + "šave": "šavi", + "manata": "manta", + "tanata": "tanta", + "lanata": "lanta", + "sanata": "santa", + "ačak": "ačka", + "ačaka": "ačka", + "ušak": "uška", + "atak": "atka", + "ataka": "atka", + "atci": "atka", + "atcima": "atka", + "etak": "etka", + "etaka": "etka", + "itak": "itka", + "itaka": "itka", + "itci": "itka", + "otak": "otka", + "otaka": "otka", + "utak": "utka", + "utaka": "utka", + "utci": "utka", + "utcima": "utka", + "eskan": "eskna", + "tičan": "tični", + "ojsci": "ojska", + "esama": "esma", + "metara": "metra", + "centar": "centra", + "centara": "centra", + "istara": "istra", + "istar": "istra", + "ošću": "osti", + "daba": "dba", + "čcima": "čka", + "čci": "čka", + "mac": "mca", + "maca": "mca", + "voljan": "voljni", + "anaka": "anki", + "vac": "vca", + "vaca": "vca", + "saca": "sca", + "sac": "sca", + "naca": "nca", + "nac": "nca", + "raca": "rca", + "rac": "rca", + "aoca": "alca", + "alaca": "alca", + "alac": "alca", + "elaca": "elca", + "elac": "elca", + "olaca": "olca", + "olac": "olca", + "olce": "olca", + "njac": "njca", + "njaca": "njca", + "ekata": "ekta", + "ekat": "ekta", + "izam": "izma", + "izama": "izma", + "jebe": "jebi", + "ašan": "ašni", +} + +type SuffixTransformationFilter struct{} + +func NewSuffixTransformationFilter() *SuffixTransformationFilter { + return &SuffixTransformationFilter{} +} + +func (s *SuffixTransformationFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := string(token.Term) + + for suffix, newSuffix := range SuffixTransformations { + if strings.HasSuffix(term, suffix) { + term = term[:len(term)-len(suffix)] + newSuffix + break + } + } + + token.Term = []byte(term) + } + + return input +} + +func SuffixTransformationFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewSuffixTransformationFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SuffixTransformationFilterName, SuffixTransformationFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hu/analyzer_hu.go b/analysis/lang/hu/analyzer_hu.go new file mode 100644 index 0000000..09ba091 --- /dev/null +++ b/analysis/lang/hu/analyzer_hu.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hu + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "hu" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopHuFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerHuFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopHuFilter, + stemmerHuFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hu/analyzer_hu_test.go b/analysis/lang/hu/analyzer_hu_test.go new file mode 100644 index 0000000..8745668 --- /dev/null +++ b/analysis/lang/hu/analyzer_hu_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hu + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestHungarianAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("babakocsi"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("babakocs"), + }, + }, + }, + { + input: []byte("babakocsijáért"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("babakocs"), + }, + }, + }, + // stop word + { + input: []byte("által"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/hu/stemmer_hu.go b/analysis/lang/hu/stemmer_hu.go new file mode 100644 index 0000000..3aadd6a --- /dev/null +++ b/analysis/lang/hu/stemmer_hu.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hu + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/hungarian" +) + +const SnowballStemmerName = "stemmer_hu_snowball" + +type HungarianStemmerFilter struct { +} + +func NewHungarianStemmerFilter() *HungarianStemmerFilter { + return &HungarianStemmerFilter{} +} + +func (s *HungarianStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + hungarian.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func HungarianStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewHungarianStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, HungarianStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hu/stop_filter_hu.go b/analysis/lang/hu/stop_filter_hu.go new file mode 100644 index 0000000..ebc6894 --- /dev/null +++ b/analysis/lang/hu/stop_filter_hu.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hu + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hu/stop_words_hu.go b/analysis/lang/hu/stop_words_hu.go new file mode 100644 index 0000000..effdc04 --- /dev/null +++ b/analysis/lang/hu/stop_words_hu.go @@ -0,0 +1,238 @@ +package hu + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_hu" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var HungarianStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/hungarian/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + +| Hungarian stop word list +| prepared by Anna Tordai + +a +ahogy +ahol +aki +akik +akkor +alatt +által +általában +amely +amelyek +amelyekben +amelyeket +amelyet +amelynek +ami +amit +amolyan +amíg +amikor +át +abban +ahhoz +annak +arra +arról +az +azok +azon +azt +azzal +azért +aztán +azután +azonban +bár +be +belül +benne +cikk +cikkek +cikkeket +csak +de +e +eddig +egész +egy +egyes +egyetlen +egyéb +egyik +egyre +ekkor +el +elég +ellen +elő +először +előtt +első +én +éppen +ebben +ehhez +emilyen +ennek +erre +ez +ezt +ezek +ezen +ezzel +ezért +és +fel +felé +hanem +hiszen +hogy +hogyan +igen +így +illetve +ill. +ill +ilyen +ilyenkor +ison +ismét +itt +jó +jól +jobban +kell +kellett +keresztül +keressünk +ki +kívül +között +közül +legalább +lehet +lehetett +legyen +lenne +lenni +lesz +lett +maga +magát +majd +majd +már +más +másik +meg +még +mellett +mert +mely +melyek +mi +mit +míg +miért +milyen +mikor +minden +mindent +mindenki +mindig +mint +mintha +mivel +most +nagy +nagyobb +nagyon +ne +néha +nekem +neki +nem +néhány +nélkül +nincs +olyan +ott +össze +ő +ők +őket +pedig +persze +rá +s +saját +sem +semmi +sok +sokat +sokkal +számára +szemben +szerint +szinte +talán +tehát +teljes +tovább +továbbá +több +úgy +ugyanis +új +újabb +újra +után +utána +utolsó +vagy +vagyis +valaki +valami +valamint +való +vagyok +van +vannak +volt +voltam +voltak +voltunk +vissza +vele +viszont +volna +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(HungarianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hy/stop_filter_hy.go b/analysis/lang/hy/stop_filter_hy.go new file mode 100644 index 0000000..eb69789 --- /dev/null +++ b/analysis/lang/hy/stop_filter_hy.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hy + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/hy/stop_words_hy.go b/analysis/lang/hy/stop_words_hy.go new file mode 100644 index 0000000..dcb6603 --- /dev/null +++ b/analysis/lang/hy/stop_words_hy.go @@ -0,0 +1,73 @@ +package hy + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_hy" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var ArmenianStopWords = []byte(`# example set of Armenian stopwords. +այդ +այլ +այն +այս +դու +դուք +եմ +են +ենք +ես +եք +է +էի +էին +էինք +էիր +էիք +էր +ըստ +թ +ի +ին +իսկ +իր +կամ +համար +հետ +հետո +մենք +մեջ +մի +ն +նա +նաև +նրա +նրանք +որ +որը +որոնք +որպես +ու +ում +պիտի +վրա +և +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(ArmenianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/id/stop_filter_id.go b/analysis/lang/id/stop_filter_id.go new file mode 100644 index 0000000..252f131 --- /dev/null +++ b/analysis/lang/id/stop_filter_id.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package id + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/id/stop_words_id.go b/analysis/lang/id/stop_words_id.go new file mode 100644 index 0000000..b85a816 --- /dev/null +++ b/analysis/lang/id/stop_words_id.go @@ -0,0 +1,386 @@ +package id + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_id" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var IndonesianStopWords = []byte(`# from appendix D of: A Study of Stemming Effects on Information +# Retrieval in Bahasa Indonesia +ada +adanya +adalah +adapun +agak +agaknya +agar +akan +akankah +akhirnya +aku +akulah +amat +amatlah +anda +andalah +antar +diantaranya +antara +antaranya +diantara +apa +apaan +mengapa +apabila +apakah +apalagi +apatah +atau +ataukah +ataupun +bagai +bagaikan +sebagai +sebagainya +bagaimana +bagaimanapun +sebagaimana +bagaimanakah +bagi +bahkan +bahwa +bahwasanya +sebaliknya +banyak +sebanyak +beberapa +seberapa +begini +beginian +beginikah +beginilah +sebegini +begitu +begitukah +begitulah +begitupun +sebegitu +belum +belumlah +sebelum +sebelumnya +sebenarnya +berapa +berapakah +berapalah +berapapun +betulkah +sebetulnya +biasa +biasanya +bila +bilakah +bisa +bisakah +sebisanya +boleh +bolehkah +bolehlah +buat +bukan +bukankah +bukanlah +bukannya +cuma +percuma +dahulu +dalam +dan +dapat +dari +daripada +dekat +demi +demikian +demikianlah +sedemikian +dengan +depan +di +dia +dialah +dini +diri +dirinya +terdiri +dong +dulu +enggak +enggaknya +entah +entahlah +terhadap +terhadapnya +hal +hampir +hanya +hanyalah +harus +haruslah +harusnya +seharusnya +hendak +hendaklah +hendaknya +hingga +sehingga +ia +ialah +ibarat +ingin +inginkah +inginkan +ini +inikah +inilah +itu +itukah +itulah +jangan +jangankan +janganlah +jika +jikalau +juga +justru +kala +kalau +kalaulah +kalaupun +kalian +kami +kamilah +kamu +kamulah +kan +kapan +kapankah +kapanpun +dikarenakan +karena +karenanya +ke +kecil +kemudian +kenapa +kepada +kepadanya +ketika +seketika +khususnya +kini +kinilah +kiranya +sekiranya +kita +kitalah +kok +lagi +lagian +selagi +lah +lain +lainnya +melainkan +selaku +lalu +melalui +terlalu +lama +lamanya +selama +selama +selamanya +lebih +terlebih +bermacam +macam +semacam +maka +makanya +makin +malah +malahan +mampu +mampukah +mana +manakala +manalagi +masih +masihkah +semasih +masing +mau +maupun +semaunya +memang +mereka +merekalah +meski +meskipun +semula +mungkin +mungkinkah +nah +namun +nanti +nantinya +nyaris +oleh +olehnya +seorang +seseorang +pada +padanya +padahal +paling +sepanjang +pantas +sepantasnya +sepantasnyalah +para +pasti +pastilah +per +pernah +pula +pun +merupakan +rupanya +serupa +saat +saatnya +sesaat +saja +sajalah +saling +bersama +sama +sesama +sambil +sampai +sana +sangat +sangatlah +saya +sayalah +se +sebab +sebabnya +sebuah +tersebut +tersebutlah +sedang +sedangkan +sedikit +sedikitnya +segala +segalanya +segera +sesegera +sejak +sejenak +sekali +sekalian +sekalipun +sesekali +sekaligus +sekarang +sekarang +sekitar +sekitarnya +sela +selain +selalu +seluruh +seluruhnya +semakin +sementara +sempat +semua +semuanya +sendiri +sendirinya +seolah +seperti +sepertinya +sering +seringnya +serta +siapa +siapakah +siapapun +disini +disinilah +sini +sinilah +sesuatu +sesuatunya +suatu +sesudah +sesudahnya +sudah +sudahkah +sudahlah +supaya +tadi +tadinya +tak +tanpa +setelah +telah +tentang +tentu +tentulah +tentunya +tertentu +seterusnya +tapi +tetapi +setiap +tiap +setidaknya +tidak +tidakkah +tidaklah +toh +waduh +wah +wahai +sewaktu +walau +walaupun +wong +yaitu +yakni +yang +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(IndonesianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/in/indic_normalize.go b/analysis/lang/in/indic_normalize.go new file mode 100644 index 0000000..f3ad7d9 --- /dev/null +++ b/analysis/lang/in/indic_normalize.go @@ -0,0 +1,51 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package in + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const NormalizeName = "normalize_in" + +type IndicNormalizeFilter struct { +} + +func NewIndicNormalizeFilter() *IndicNormalizeFilter { + return &IndicNormalizeFilter{} +} + +func (s *IndicNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runes := bytes.Runes(token.Term) + runes = normalize(runes) + token.Term = analysis.BuildTermFromRunes(runes) + } + return input +} + +func NormalizerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewIndicNormalizeFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(NormalizeName, NormalizerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/in/indic_normalize_test.go b/analysis/lang/in/indic_normalize_test.go new file mode 100644 index 0000000..5789eb7 --- /dev/null +++ b/analysis/lang/in/indic_normalize_test.go @@ -0,0 +1,138 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package in + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestIndicNormalizeFilter(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + // basics + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अाॅअाॅ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ऑऑ"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अाॆअाॆ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ऒऒ"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अाेअाे"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ओओ"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अाैअाै"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("औऔ"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अाअा"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("आआ"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("अाैर"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("और"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ত্‍"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ৎ"), + }, + }, + }, + // empty term + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte(""), + }, + }, + }, + } + + indicNormalizeFilter := NewIndicNormalizeFilter() + for _, test := range tests { + actual := indicNormalizeFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + t.Errorf("expected % x, got % x for % x", test.output[0].Term, actual[0].Term, test.input[0].Term) + t.Errorf("expected %s, got %s for %s", test.output[0].Term, actual[0].Term, test.input[0].Term) + } + } +} diff --git a/analysis/lang/in/scripts.go b/analysis/lang/in/scripts.go new file mode 100644 index 0000000..cc32f10 --- /dev/null +++ b/analysis/lang/in/scripts.go @@ -0,0 +1,296 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package in + +import ( + "unicode" + + "github.com/bits-and-blooms/bitset" + "github.com/blevesearch/bleve/v2/analysis" +) + +type ScriptData struct { + flag rune + base rune + decompMask *bitset.BitSet +} + +var scripts = map[*unicode.RangeTable]*ScriptData{ + unicode.Devanagari: { + flag: 1, + base: 0x0900, + }, + unicode.Bengali: { + flag: 2, + base: 0x0980, + }, + unicode.Gurmukhi: { + flag: 4, + base: 0x0A00, + }, + unicode.Gujarati: { + flag: 8, + base: 0x0A80, + }, + unicode.Oriya: { + flag: 16, + base: 0x0B00, + }, + unicode.Tamil: { + flag: 32, + base: 0x0B80, + }, + unicode.Telugu: { + flag: 64, + base: 0x0C00, + }, + unicode.Kannada: { + flag: 128, + base: 0x0C80, + }, + unicode.Malayalam: { + flag: 256, + base: 0x0D00, + }, +} + +func flag(ub *unicode.RangeTable) rune { + return scripts[ub].flag +} + +var decompositions = [][]rune{ + /* devanagari, gujarati vowel candra O */ + {0x05, 0x3E, 0x45, 0x11, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari short O */ + {0x05, 0x3E, 0x46, 0x12, flag(unicode.Devanagari)}, + /* devanagari, gujarati letter O */ + {0x05, 0x3E, 0x47, 0x13, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari letter AI, gujarati letter AU */ + {0x05, 0x3E, 0x48, 0x14, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari, bengali, gurmukhi, gujarati, oriya AA */ + {0x05, 0x3E, -1, 0x06, flag(unicode.Devanagari) | flag(unicode.Bengali) | flag(unicode.Gurmukhi) | flag(unicode.Gujarati) | flag(unicode.Oriya)}, + /* devanagari letter candra A */ + {0x05, 0x45, -1, 0x72, flag(unicode.Devanagari)}, + /* gujarati vowel candra E */ + {0x05, 0x45, -1, 0x0D, flag(unicode.Gujarati)}, + /* devanagari letter short A */ + {0x05, 0x46, -1, 0x04, flag(unicode.Devanagari)}, + /* gujarati letter E */ + {0x05, 0x47, -1, 0x0F, flag(unicode.Gujarati)}, + /* gurmukhi, gujarati letter AI */ + {0x05, 0x48, -1, 0x10, flag(unicode.Gurmukhi) | flag(unicode.Gujarati)}, + /* devanagari, gujarati vowel candra O */ + {0x05, 0x49, -1, 0x11, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari short O */ + {0x05, 0x4A, -1, 0x12, flag(unicode.Devanagari)}, + /* devanagari, gujarati letter O */ + {0x05, 0x4B, -1, 0x13, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari letter AI, gurmukhi letter AU, gujarati letter AU */ + {0x05, 0x4C, -1, 0x14, flag(unicode.Devanagari) | flag(unicode.Gurmukhi) | flag(unicode.Gujarati)}, + /* devanagari, gujarati vowel candra O */ + {0x06, 0x45, -1, 0x11, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari short O */ + {0x06, 0x46, -1, 0x12, flag(unicode.Devanagari)}, + /* devanagari, gujarati letter O */ + {0x06, 0x47, -1, 0x13, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari letter AI, gujarati letter AU */ + {0x06, 0x48, -1, 0x14, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* malayalam letter II */ + {0x07, 0x57, -1, 0x08, flag(unicode.Malayalam)}, + /* devanagari letter UU */ + {0x09, 0x41, -1, 0x0A, flag(unicode.Devanagari)}, + /* tamil, malayalam letter UU (some styles) */ + {0x09, 0x57, -1, 0x0A, flag(unicode.Tamil) | flag(unicode.Malayalam)}, + /* malayalam letter AI */ + {0x0E, 0x46, -1, 0x10, flag(unicode.Malayalam)}, + /* devanagari candra E */ + {0x0F, 0x45, -1, 0x0D, flag(unicode.Devanagari)}, + /* devanagari short E */ + {0x0F, 0x46, -1, 0x0E, flag(unicode.Devanagari)}, + /* devanagari AI */ + {0x0F, 0x47, -1, 0x10, flag(unicode.Devanagari)}, + /* oriya AI */ + {0x0F, 0x57, -1, 0x10, flag(unicode.Oriya)}, + /* malayalam letter OO */ + {0x12, 0x3E, -1, 0x13, flag(unicode.Malayalam)}, + /* telugu, kannada letter AU */ + {0x12, 0x4C, -1, 0x14, flag(unicode.Telugu) | flag(unicode.Kannada)}, + /* telugu letter OO */ + {0x12, 0x55, -1, 0x13, flag(unicode.Telugu)}, + /* tamil, malayalam letter AU */ + {0x12, 0x57, -1, 0x14, flag(unicode.Tamil) | flag(unicode.Malayalam)}, + /* oriya letter AU */ + {0x13, 0x57, -1, 0x14, flag(unicode.Oriya)}, + /* devanagari qa */ + {0x15, 0x3C, -1, 0x58, flag(unicode.Devanagari)}, + /* devanagari, gurmukhi khha */ + {0x16, 0x3C, -1, 0x59, flag(unicode.Devanagari) | flag(unicode.Gurmukhi)}, + /* devanagari, gurmukhi ghha */ + {0x17, 0x3C, -1, 0x5A, flag(unicode.Devanagari) | flag(unicode.Gurmukhi)}, + /* devanagari, gurmukhi za */ + {0x1C, 0x3C, -1, 0x5B, flag(unicode.Devanagari) | flag(unicode.Gurmukhi)}, + /* devanagari dddha, bengali, oriya rra */ + {0x21, 0x3C, -1, 0x5C, flag(unicode.Devanagari) | flag(unicode.Bengali) | flag(unicode.Oriya)}, + /* devanagari, bengali, oriya rha */ + {0x22, 0x3C, -1, 0x5D, flag(unicode.Devanagari) | flag(unicode.Bengali) | flag(unicode.Oriya)}, + /* malayalam chillu nn */ + {0x23, 0x4D, 0xFF, 0x7A, flag(unicode.Malayalam)}, + /* bengali khanda ta */ + {0x24, 0x4D, 0xFF, 0x4E, flag(unicode.Bengali)}, + /* devanagari nnna */ + {0x28, 0x3C, -1, 0x29, flag(unicode.Devanagari)}, + /* malayalam chillu n */ + {0x28, 0x4D, 0xFF, 0x7B, flag(unicode.Malayalam)}, + /* devanagari, gurmukhi fa */ + {0x2B, 0x3C, -1, 0x5E, flag(unicode.Devanagari) | flag(unicode.Gurmukhi)}, + /* devanagari, bengali yya */ + {0x2F, 0x3C, -1, 0x5F, flag(unicode.Devanagari) | flag(unicode.Bengali)}, + /* telugu letter vocalic R */ + {0x2C, 0x41, 0x41, 0x0B, flag(unicode.Telugu)}, + /* devanagari rra */ + {0x30, 0x3C, -1, 0x31, flag(unicode.Devanagari)}, + /* malayalam chillu rr */ + {0x30, 0x4D, 0xFF, 0x7C, flag(unicode.Malayalam)}, + /* malayalam chillu l */ + {0x32, 0x4D, 0xFF, 0x7D, flag(unicode.Malayalam)}, + /* devanagari llla */ + {0x33, 0x3C, -1, 0x34, flag(unicode.Devanagari)}, + /* malayalam chillu ll */ + {0x33, 0x4D, 0xFF, 0x7E, flag(unicode.Malayalam)}, + /* telugu letter MA */ + {0x35, 0x41, -1, 0x2E, flag(unicode.Telugu)}, + /* devanagari, gujarati vowel sign candra O */ + {0x3E, 0x45, -1, 0x49, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari vowel sign short O */ + {0x3E, 0x46, -1, 0x4A, flag(unicode.Devanagari)}, + /* devanagari, gujarati vowel sign O */ + {0x3E, 0x47, -1, 0x4B, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* devanagari, gujarati vowel sign AU */ + {0x3E, 0x48, -1, 0x4C, flag(unicode.Devanagari) | flag(unicode.Gujarati)}, + /* kannada vowel sign II */ + {0x3F, 0x55, -1, 0x40, flag(unicode.Kannada)}, + /* gurmukhi vowel sign UU (when stacking) */ + {0x41, 0x41, -1, 0x42, flag(unicode.Gurmukhi)}, + /* tamil, malayalam vowel sign O */ + {0x46, 0x3E, -1, 0x4A, flag(unicode.Tamil) | flag(unicode.Malayalam)}, + /* kannada vowel sign OO */ + {0x46, 0x42, 0x55, 0x4B, flag(unicode.Kannada)}, + /* kannada vowel sign O */ + {0x46, 0x42, -1, 0x4A, flag(unicode.Kannada)}, + /* malayalam vowel sign AI (if reordered twice) */ + {0x46, 0x46, -1, 0x48, flag(unicode.Malayalam)}, + /* telugu, kannada vowel sign EE */ + {0x46, 0x55, -1, 0x47, flag(unicode.Telugu) | flag(unicode.Kannada)}, + /* telugu, kannada vowel sign AI */ + {0x46, 0x56, -1, 0x48, flag(unicode.Telugu) | flag(unicode.Kannada)}, + /* tamil, malayalam vowel sign AU */ + {0x46, 0x57, -1, 0x4C, flag(unicode.Tamil) | flag(unicode.Malayalam)}, + /* bengali, oriya vowel sign O, tamil, malayalam vowel sign OO */ + {0x47, 0x3E, -1, 0x4B, flag(unicode.Bengali) | flag(unicode.Oriya) | flag(unicode.Tamil) | flag(unicode.Malayalam)}, + /* bengali, oriya vowel sign AU */ + {0x47, 0x57, -1, 0x4C, flag(unicode.Bengali) | flag(unicode.Oriya)}, + /* kannada vowel sign OO */ + {0x4A, 0x55, -1, 0x4B, flag(unicode.Kannada)}, + /* gurmukhi letter I */ + {0x72, 0x3F, -1, 0x07, flag(unicode.Gurmukhi)}, + /* gurmukhi letter II */ + {0x72, 0x40, -1, 0x08, flag(unicode.Gurmukhi)}, + /* gurmukhi letter EE */ + {0x72, 0x47, -1, 0x0F, flag(unicode.Gurmukhi)}, + /* gurmukhi letter U */ + {0x73, 0x41, -1, 0x09, flag(unicode.Gurmukhi)}, + /* gurmukhi letter UU */ + {0x73, 0x42, -1, 0x0A, flag(unicode.Gurmukhi)}, + /* gurmukhi letter OO */ + {0x73, 0x4B, -1, 0x13, flag(unicode.Gurmukhi)}, +} + +func init() { + for _, scriptData := range scripts { + scriptData.decompMask = bitset.New(0x7d) + for _, decomposition := range decompositions { + ch := decomposition[0] + flags := decomposition[4] + if (flags & scriptData.flag) != 0 { + scriptData.decompMask.Set(uint(ch)) + } + } + } +} + +func lookupScript(r rune) *unicode.RangeTable { + for script := range scripts { + if unicode.Is(script, r) { + return script + } + } + return nil +} + +func normalize(input []rune) []rune { + inputLen := len(input) + for i := 0; i < inputLen; i++ { + r := input[i] + script := lookupScript(r) + if script != nil { + scriptData := scripts[script] + ch := r - scriptData.base + if scriptData.decompMask.Test(uint(ch)) { + input = compose(ch, script, scriptData, input, i, inputLen) + inputLen = len(input) + } + } + } + return input[0:inputLen] +} + +func compose(ch0 rune, script0 *unicode.RangeTable, scriptData *ScriptData, input []rune, pos int, inputLen int) []rune { + if pos+1 >= inputLen { + return input // need at least 2 characters + } + + ch1 := input[pos+1] - scriptData.base + script1 := lookupScript(input[pos+1]) + if script0 != script1 { + return input // need to be same script + } + + ch2 := rune(-1) + if pos+2 < inputLen { + ch2 = input[pos+2] - scriptData.base + script2 := lookupScript(input[pos+2]) + if input[pos+2] == '\u200D' { + ch2 = 0xff // zero width joiner + } else if script2 != script1 { + ch2 = -1 // still allow 2 character match + } + } + + for _, decomposition := range decompositions { + if decomposition[0] == ch0 && + (decomposition[4]&scriptData.flag) != 0 { + if decomposition[1] == ch1 && + (decomposition[2] < 0 || decomposition[2] == ch2) { + input[pos] = scriptData.base + decomposition[3] + input = analysis.DeleteRune(input, pos+1) + if decomposition[2] >= 0 { + input = analysis.DeleteRune(input, pos+1) + } + return input + } + } + } + return input +} diff --git a/analysis/lang/it/analyzer_it.go b/analysis/lang/it/analyzer_it.go new file mode 100644 index 0000000..8531166 --- /dev/null +++ b/analysis/lang/it/analyzer_it.go @@ -0,0 +1,65 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "it" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + elisionFilter, err := cache.TokenFilterNamed(ElisionName) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopItFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerItFilter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + elisionFilter, + stopItFilter, + stemmerItFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/it/analyzer_it_test.go b/analysis/lang/it/analyzer_it_test.go new file mode 100644 index 0000000..19b9d4d --- /dev/null +++ b/analysis/lang/it/analyzer_it_test.go @@ -0,0 +1,96 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestItalianAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("abbandonata"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abbandonat"), + }, + }, + }, + { + input: []byte("abbandonati"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abbandonat"), + }, + }, + }, + // stop word + { + input: []byte("dallo"), + output: analysis.TokenStream{}, + }, + // contractions + { + input: []byte("dell'Italia"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ital"), + }, + }, + }, + { + input: []byte("l'Italiano"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("italian"), + }, + }, + }, + // test for bug #218 + { + input: []byte("Nell'anfora"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("anfor"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/it/articles_it.go b/analysis/lang/it/articles_it.go new file mode 100644 index 0000000..08d3c01 --- /dev/null +++ b/analysis/lang/it/articles_it.go @@ -0,0 +1,48 @@ +package it + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const ArticlesName = "articles_it" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis + +var ItalianArticles = []byte(` +c +l +all +dall +dell +nell +sull +coll +pell +gl +agl +dagl +degl +negl +sugl +un +m +t +s +v +d +`) + +func ArticlesTokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(ItalianArticles) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(ArticlesName, ArticlesTokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/it/elision_it.go b/analysis/lang/it/elision_it.go new file mode 100644 index 0000000..022e9c6 --- /dev/null +++ b/analysis/lang/it/elision_it.go @@ -0,0 +1,40 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/elision" + "github.com/blevesearch/bleve/v2/registry" +) + +const ElisionName = "elision_it" + +func ElisionFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + articlesTokenMap, err := cache.TokenMapNamed(ArticlesName) + if err != nil { + return nil, fmt.Errorf("error building elision filter: %v", err) + } + return elision.NewElisionFilter(articlesTokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(ElisionName, ElisionFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/it/elision_it_test.go b/analysis/lang/it/elision_it_test.go new file mode 100644 index 0000000..b79610d --- /dev/null +++ b/analysis/lang/it/elision_it_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestItalianElision(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("dell'Italia"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Italia"), + }, + }, + }, + } + + cache := registry.NewCache() + elisionFilter, err := cache.TokenFilterNamed(ElisionName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := elisionFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/it/light_stemmer_it.go b/analysis/lang/it/light_stemmer_it.go new file mode 100644 index 0000000..57c1564 --- /dev/null +++ b/analysis/lang/it/light_stemmer_it.go @@ -0,0 +1,104 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const LightStemmerName = "stemmer_it_light" + +type ItalianLightStemmerFilter struct { +} + +func NewItalianLightStemmerFilterFilter() *ItalianLightStemmerFilter { + return &ItalianLightStemmerFilter{} +} + +func (s *ItalianLightStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runes := bytes.Runes(token.Term) + runes = stem(runes) + token.Term = analysis.BuildTermFromRunes(runes) + } + return input +} + +func stem(input []rune) []rune { + + inputLen := len(input) + + if inputLen < 6 { + return input + } + + for i := 0; i < inputLen; i++ { + switch input[i] { + case 'à', 'á', 'â', 'ä': + input[i] = 'a' + case 'ò', 'ó', 'ô', 'ö': + input[i] = 'o' + case 'è', 'é', 'ê', 'ë': + input[i] = 'e' + case 'ù', 'ú', 'û', 'ü': + input[i] = 'u' + case 'ì', 'í', 'î', 'ï': + input[i] = 'i' + } + } + + switch input[inputLen-1] { + case 'e': + if input[inputLen-2] == 'i' || input[inputLen-2] == 'h' { + return input[0 : inputLen-2] + } else { + return input[0 : inputLen-1] + } + case 'i': + if input[inputLen-2] == 'h' || input[inputLen-2] == 'i' { + return input[0 : inputLen-2] + } else { + return input[0 : inputLen-1] + } + case 'a': + if input[inputLen-2] == 'i' { + return input[0 : inputLen-2] + } else { + return input[0 : inputLen-1] + } + case 'o': + if input[inputLen-2] == 'i' { + return input[0 : inputLen-2] + } else { + return input[0 : inputLen-1] + } + } + + return input +} + +func ItalianLightStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewItalianLightStemmerFilterFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(LightStemmerName, ItalianLightStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/it/light_stemmer_it_test.go b/analysis/lang/it/light_stemmer_it_test.go new file mode 100644 index 0000000..5122f19 --- /dev/null +++ b/analysis/lang/it/light_stemmer_it_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestItalianLightStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ragazzo"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ragazz"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ragazzi"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ragazz"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/it/stemmer_it_snowball.go b/analysis/lang/it/stemmer_it_snowball.go new file mode 100644 index 0000000..fd51641 --- /dev/null +++ b/analysis/lang/it/stemmer_it_snowball.go @@ -0,0 +1,52 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/italian" +) + +const SnowballStemmerName = "stemmer_it_snowball" + +type ItalianStemmerFilter struct { +} + +func NewItalianStemmerFilter() *ItalianStemmerFilter { + return &ItalianStemmerFilter{} +} + +func (s *ItalianStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + italian.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func ItalianStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewItalianStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, ItalianStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/it/stemmer_it_snowball_test.go b/analysis/lang/it/stemmer_it_snowball_test.go new file mode 100644 index 0000000..134f2e0 --- /dev/null +++ b/analysis/lang/it/stemmer_it_snowball_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSnowballItalianStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aizzata"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aizz"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aizzargli"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aizz"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aizzasse"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("aizz"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/it/stop_filter_it.go b/analysis/lang/it/stop_filter_it.go new file mode 100644 index 0000000..73e297f --- /dev/null +++ b/analysis/lang/it/stop_filter_it.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package it + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/it/stop_words_it.go b/analysis/lang/it/stop_words_it.go new file mode 100644 index 0000000..b167f4d --- /dev/null +++ b/analysis/lang/it/stop_words_it.go @@ -0,0 +1,330 @@ +package it + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_it" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var ItalianStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/italian/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | An Italian stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + +ad | a (to) before vowel +al | a + il +allo | a + lo +ai | a + i +agli | a + gli +all | a + l' +agl | a + gl' +alla | a + la +alle | a + le +con | with +col | con + il +coi | con + i (forms collo, cogli etc are now very rare) +da | from +dal | da + il +dallo | da + lo +dai | da + i +dagli | da + gli +dall | da + l' +dagl | da + gll' +dalla | da + la +dalle | da + le +di | of +del | di + il +dello | di + lo +dei | di + i +degli | di + gli +dell | di + l' +degl | di + gl' +della | di + la +delle | di + le +in | in +nel | in + el +nello | in + lo +nei | in + i +negli | in + gli +nell | in + l' +negl | in + gl' +nella | in + la +nelle | in + le +su | on +sul | su + il +sullo | su + lo +sui | su + i +sugli | su + gli +sull | su + l' +sugl | su + gl' +sulla | su + la +sulle | su + le +per | through, by +tra | among +contro | against +io | I +tu | thou +lui | he +lei | she +noi | we +voi | you +loro | they +mio | my +mia | +miei | +mie | +tuo | +tua | +tuoi | thy +tue | +suo | +sua | +suoi | his, her +sue | +nostro | our +nostra | +nostri | +nostre | +vostro | your +vostra | +vostri | +vostre | +mi | me +ti | thee +ci | us, there +vi | you, there +lo | him, the +la | her, the +li | them +le | them, the +gli | to him, the +ne | from there etc +il | the +un | a +uno | a +una | a +ma | but +ed | and +se | if +perché | why, because +anche | also +come | how +dov | where (as dov') +dove | where +che | who, that +chi | who +cui | whom +non | not +più | more +quale | who, that +quanto | how much +quanti | +quanta | +quante | +quello | that +quelli | +quella | +quelle | +questo | this +questi | +questa | +queste | +si | yes +tutto | all +tutti | all + + | single letter forms: + +a | at +c | as c' for ce or ci +e | and +i | the +l | as l' +o | or + + | forms of avere, to have (not including the infinitive): + +ho +hai +ha +abbiamo +avete +hanno +abbia +abbiate +abbiano +avrò +avrai +avrà +avremo +avrete +avranno +avrei +avresti +avrebbe +avremmo +avreste +avrebbero +avevo +avevi +aveva +avevamo +avevate +avevano +ebbi +avesti +ebbe +avemmo +aveste +ebbero +avessi +avesse +avessimo +avessero +avendo +avuto +avuta +avuti +avute + + | forms of essere, to be (not including the infinitive): +sono +sei +è +siamo +siete +sia +siate +siano +sarò +sarai +sarà +saremo +sarete +saranno +sarei +saresti +sarebbe +saremmo +sareste +sarebbero +ero +eri +era +eravamo +eravate +erano +fui +fosti +fu +fummo +foste +furono +fossi +fosse +fossimo +fossero +essendo + + | forms of fare, to do (not including the infinitive, fa, fat-): +faccio +fai +facciamo +fanno +faccia +facciate +facciano +farò +farai +farà +faremo +farete +faranno +farei +faresti +farebbe +faremmo +fareste +farebbero +facevo +facevi +faceva +facevamo +facevate +facevano +feci +facesti +fece +facemmo +faceste +fecero +facessi +facesse +facessimo +facessero +facendo + + | forms of stare, to be (not including the infinitive): +sto +stai +sta +stiamo +stanno +stia +stiate +stiano +starò +starai +starà +staremo +starete +staranno +starei +staresti +starebbe +staremmo +stareste +starebbero +stavo +stavi +stava +stavamo +stavate +stavano +stetti +stesti +stette +stemmo +steste +stettero +stessi +stesse +stessimo +stessero +stando +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(ItalianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/nl/analyzer_nl.go b/analysis/lang/nl/analyzer_nl.go new file mode 100644 index 0000000..22a6fbf --- /dev/null +++ b/analysis/lang/nl/analyzer_nl.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "nl" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopNlFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerNlFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopNlFilter, + stemmerNlFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/nl/analyzer_nl_test.go b/analysis/lang/nl/analyzer_nl_test.go new file mode 100644 index 0000000..707655f --- /dev/null +++ b/analysis/lang/nl/analyzer_nl_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nl + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestDutchAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("lichamelijk"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("licham"), + }, + }, + }, + { + input: []byte("lichamelijke"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("licham"), + }, + }, + }, + // stop word + { + input: []byte("van"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/nl/stemmer_nl.go b/analysis/lang/nl/stemmer_nl.go new file mode 100644 index 0000000..5612965 --- /dev/null +++ b/analysis/lang/nl/stemmer_nl.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/dutch" +) + +const SnowballStemmerName = "stemmer_nl_snowball" + +type DutchStemmerFilter struct { +} + +func NewDutchStemmerFilter() *DutchStemmerFilter { + return &DutchStemmerFilter{} +} + +func (s *DutchStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + dutch.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func DutchStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewDutchStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, DutchStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/nl/stop_filter_nl.go b/analysis/lang/nl/stop_filter_nl.go new file mode 100644 index 0000000..31fa91e --- /dev/null +++ b/analysis/lang/nl/stop_filter_nl.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/nl/stop_words_nl.go b/analysis/lang/nl/stop_words_nl.go new file mode 100644 index 0000000..bb7e6de --- /dev/null +++ b/analysis/lang/nl/stop_words_nl.go @@ -0,0 +1,146 @@ +package nl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_nl" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var DutchStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/dutch/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A Dutch stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + | This is a ranked list (commonest to rarest) of stopwords derived from + | a large sample of Dutch text. + + | Dutch stop words frequently exhibit homonym clashes. These are indicated + | clearly below. + +de | the +en | and +van | of, from +ik | I, the ego +te | (1) chez, at etc, (2) to, (3) too +dat | that, which +die | that, those, who, which +in | in, inside +een | a, an, one +hij | he +het | the, it +niet | not, nothing, naught +zijn | (1) to be, being, (2) his, one's, its +is | is +was | (1) was, past tense of all persons sing. of 'zijn' (to be) (2) wax, (3) the washing, (4) rise of river +op | on, upon, at, in, up, used up +aan | on, upon, to (as dative) +met | with, by +als | like, such as, when +voor | (1) before, in front of, (2) furrow +had | had, past tense all persons sing. of 'hebben' (have) +er | there +maar | but, only +om | round, about, for etc +hem | him +dan | then +zou | should/would, past tense all persons sing. of 'zullen' +of | or, whether, if +wat | what, something, anything +mijn | possessive and noun 'mine' +men | people, 'one' +dit | this +zo | so, thus, in this way +door | through by +over | over, across +ze | she, her, they, them +zich | oneself +bij | (1) a bee, (2) by, near, at +ook | also, too +tot | till, until +je | you +mij | me +uit | out of, from +der | Old Dutch form of 'van der' still found in surnames +daar | (1) there, (2) because +haar | (1) her, their, them, (2) hair +naar | (1) unpleasant, unwell etc, (2) towards, (3) as +heb | present first person sing. of 'to have' +hoe | how, why +heeft | present third person sing. of 'to have' +hebben | 'to have' and various parts thereof +deze | this +u | you +want | (1) for, (2) mitten, (3) rigging +nog | yet, still +zal | 'shall', first and third person sing. of verb 'zullen' (will) +me | me +zij | she, they +nu | now +ge | 'thou', still used in Belgium and south Netherlands +geen | none +omdat | because +iets | something, somewhat +worden | to become, grow, get +toch | yet, still +al | all, every, each +waren | (1) 'were' (2) to wander, (3) wares, (3) +veel | much, many +meer | (1) more, (2) lake +doen | to do, to make +toen | then, when +moet | noun 'spot/mote' and present form of 'to must' +ben | (1) am, (2) 'are' in interrogative second person singular of 'to be' +zonder | without +kan | noun 'can' and present form of 'to be able' +hun | their, them +dus | so, consequently +alles | all, everything, anything +onder | under, beneath +ja | yes, of course +eens | once, one day +hier | here +wie | who +werd | imperfect third person sing. of 'become' +altijd | always +doch | yet, but etc +wordt | present third person sing. of 'become' +wezen | (1) to be, (2) 'been' as in 'been fishing', (3) orphans +kunnen | to be able +ons | us/our +zelf | self +tegen | against, towards, at +na | after, near +reeds | already +wil | (1) present tense of 'want', (2) 'will', noun, (3) fender +kon | could; past tense of 'to be able' +niets | nothing +uw | your +iemand | somebody +geweest | been; past participle of 'be' +andere | other +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(DutchStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/no/analyzer_no.go b/analysis/lang/no/analyzer_no.go new file mode 100644 index 0000000..64bf71c --- /dev/null +++ b/analysis/lang/no/analyzer_no.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package no + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "no" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopNoFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerNoFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopNoFilter, + stemmerNoFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/no/analyzer_no_test.go b/analysis/lang/no/analyzer_no_test.go new file mode 100644 index 0000000..b37cb4d --- /dev/null +++ b/analysis/lang/no/analyzer_no_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package no + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestNorwegianAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("havnedistriktene"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("havnedistrikt"), + }, + }, + }, + { + input: []byte("havnedistrikter"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("havnedistrikt"), + }, + }, + }, + // stop word + { + input: []byte("det"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/no/stemmer_no.go b/analysis/lang/no/stemmer_no.go new file mode 100644 index 0000000..97e06d4 --- /dev/null +++ b/analysis/lang/no/stemmer_no.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package no + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/norwegian" +) + +const SnowballStemmerName = "stemmer_no_snowball" + +type NorwegianStemmerFilter struct { +} + +func NewNorwegianStemmerFilter() *NorwegianStemmerFilter { + return &NorwegianStemmerFilter{} +} + +func (s *NorwegianStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + norwegian.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func NorwegianStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewNorwegianStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, NorwegianStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/no/stop_filter_no.go b/analysis/lang/no/stop_filter_no.go new file mode 100644 index 0000000..a698c62 --- /dev/null +++ b/analysis/lang/no/stop_filter_no.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package no + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/no/stop_words_no.go b/analysis/lang/no/stop_words_no.go new file mode 100644 index 0000000..4b58ab9 --- /dev/null +++ b/analysis/lang/no/stop_words_no.go @@ -0,0 +1,221 @@ +package no + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_no" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var NorwegianStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/norwegian/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A Norwegian stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + | This stop word list is for the dominant bokmål dialect. Words unique + | to nynorsk are marked *. + + | Revised by Jan Bruusgaard , Jan 2005 + +og | and +i | in +jeg | I +det | it/this/that +at | to (w. inf.) +en | a/an +et | a/an +den | it/this/that +til | to +er | is/am/are +som | who/that +på | on +de | they / you(formal) +med | with +han | he +av | of +ikke | not +ikkje | not * +der | there +så | so +var | was/were +meg | me +seg | you +men | but +ett | one +har | have +om | about +vi | we +min | my +mitt | my +ha | have +hadde | had +hun | she +nå | now +over | over +da | when/as +ved | by/know +fra | from +du | you +ut | out +sin | your +dem | them +oss | us +opp | up +man | you/one +kan | can +hans | his +hvor | where +eller | or +hva | what +skal | shall/must +selv | self (reflective) +sjøl | self (reflective) +her | here +alle | all +vil | will +bli | become +ble | became +blei | became * +blitt | have become +kunne | could +inn | in +når | when +være | be +kom | come +noen | some +noe | some +ville | would +dere | you +som | who/which/that +deres | their/theirs +kun | only/just +ja | yes +etter | after +ned | down +skulle | should +denne | this +for | for/because +deg | you +si | hers/his +sine | hers/his +sitt | hers/his +mot | against +å | to +meget | much +hvorfor | why +dette | this +disse | these/those +uten | without +hvordan | how +ingen | none +din | your +ditt | your +blir | become +samme | same +hvilken | which +hvilke | which (plural) +sånn | such a +inni | inside/within +mellom | between +vår | our +hver | each +hvem | who +vors | us/ours +hvis | whose +både | both +bare | only/just +enn | than +fordi | as/because +før | before +mange | many +også | also +slik | just +vært | been +være | to be +båe | both * +begge | both +siden | since +dykk | your * +dykkar | yours * +dei | they * +deira | them * +deires | theirs * +deim | them * +di | your (fem.) * +då | as/when * +eg | I * +ein | a/an * +eit | a/an * +eitt | a/an * +elles | or * +honom | he * +hjå | at * +ho | she * +hoe | she * +henne | her +hennar | her/hers +hennes | hers +hoss | how * +hossen | how * +ikkje | not * +ingi | noone * +inkje | noone * +korleis | how * +korso | how * +kva | what/which * +kvar | where * +kvarhelst | where * +kven | who/whom * +kvi | why * +kvifor | why * +me | we * +medan | while * +mi | my * +mine | my * +mykje | much * +no | now * +nokon | some (masc./neut.) * +noka | some (fem.) * +nokor | some * +noko | some * +nokre | some * +si | his/hers * +sia | since * +sidan | since * +so | so * +somt | some * +somme | some * +um | about* +upp | up * +vere | be * +vore | was * +verte | become * +vort | become * +varte | became * +vart | became * + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(NorwegianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pl/analyzer_pl.go b/analysis/lang/pl/analyzer_pl.go new file mode 100644 index 0000000..0e202e8 --- /dev/null +++ b/analysis/lang/pl/analyzer_pl.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "pl" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopPlFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerPlFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopPlFilter, + stemmerPlFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pl/analyzer_pl_test.go b/analysis/lang/pl/analyzer_pl_test.go new file mode 100644 index 0000000..073a28f --- /dev/null +++ b/analysis/lang/pl/analyzer_pl_test.go @@ -0,0 +1,149 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pl + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestPolishAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("śmiało"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("śmieć"), + }, + }, + }, + { + input: []byte("przypadku"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("przypadek"), + }, + }, + }, + // stop word + { + input: []byte("według"), + output: analysis.TokenStream{}, + }, + // digits safe + { + input: []byte("text 1000"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("text"), + }, + &analysis.Token{ + Term: []byte("1000"), + }, + }, + }, + { + input: []byte("badawczego było opracowanie kompendium które przystępny sposób prezentowało niespecjalistom zakresu kryptografii kwantowej wykorzystanie technik kwantowych do bezpiecznego przesyłu przetwarzania informacji"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("badawczy"), + }, + &analysis.Token{ + Term: []byte("opracować"), + }, + &analysis.Token{ + Term: []byte("kompendium"), + }, + &analysis.Token{ + Term: []byte("przystyć"), + }, + &analysis.Token{ + Term: []byte("prezentować"), + }, + &analysis.Token{ + Term: []byte("niespecjalista"), + }, + &analysis.Token{ + Term: []byte("zakres"), + }, + &analysis.Token{ + Term: []byte("kryptografia"), + }, + &analysis.Token{ + Term: []byte("kwantowy"), + }, + &analysis.Token{ + Term: []byte("wykorzyseć"), + }, + &analysis.Token{ + Term: []byte("technika"), + }, + &analysis.Token{ + Term: []byte("kwantowy"), + }, + &analysis.Token{ + Term: []byte("bezpieczny"), + }, + &analysis.Token{ + Term: []byte("przesył"), + }, + &analysis.Token{ + Term: []byte("przetwarzać"), + }, + &analysis.Token{ + Term: []byte("informacja"), + }, + }, + }, + { + input: []byte("Ale ta wiedza była utrzymywana w tajemnicy"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("wiedza"), + }, + &analysis.Token{ + Term: []byte("utrzymywać"), + }, + &analysis.Token{ + Term: []byte("tajemnik"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/pl/stemmer_pl.go b/analysis/lang/pl/stemmer_pl.go new file mode 100644 index 0000000..c997fb1 --- /dev/null +++ b/analysis/lang/pl/stemmer_pl.go @@ -0,0 +1,58 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/lang/pl/stempel" + "github.com/blevesearch/bleve/v2/registry" +) + +const SnowballStemmerName = "stemmer_pl" + +type PolishStemmerFilter struct { + trie stempel.Trie +} + +func NewPolishStemmerFilter() (*PolishStemmerFilter, error) { + trie, err := stempel.LoadTrie() + if err != nil { + return nil, err + } + return &PolishStemmerFilter{ + trie: trie, + }, nil +} + +func (s *PolishStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + buff := []rune(string(token.Term)) + diff := s.trie.GetLastOnPath(buff) + buff = stempel.Diff(buff, diff) + token.Term = []byte(string(buff)) + } + return input +} + +func PolishStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewPolishStemmerFilter() +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, PolishStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pl/stemmer_pl_test.go b/analysis/lang/pl/stemmer_pl_test.go new file mode 100644 index 0000000..dcfed34 --- /dev/null +++ b/analysis/lang/pl/stemmer_pl_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pl + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestPolishStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("utrzymywana"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("utrzymywać"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("tajemnicy"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("tajemnik"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/pl/stempel/LICENSE b/analysis/lang/pl/stempel/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/analysis/lang/pl/stempel/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/analysis/lang/pl/stempel/cell.go b/analysis/lang/pl/stempel/cell.go new file mode 100644 index 0000000..b090571 --- /dev/null +++ b/analysis/lang/pl/stempel/cell.go @@ -0,0 +1,53 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "fmt" + + "github.com/blevesearch/stempel/javadata" +) + +type cell struct { + ref int32 + cmd int32 +} + +func (c *cell) String() string { + return fmt.Sprintf("ref(%d) cmd(%d)", c.ref, c.cmd) +} + +func newCell(r *javadata.Reader) (*cell, error) { + cmd, err := r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading cell cmd: %v", err) + } + _, err = r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading cell cnt: %v", err) + } + ref, err := r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading cell ref: %v", err) + } + _, err = r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading cell skip: %v", err) + } + return &cell{ + cmd: cmd, + ref: ref, + }, nil +} diff --git a/analysis/lang/pl/stempel/diff.go b/analysis/lang/pl/stempel/diff.go new file mode 100644 index 0000000..5b1ce25 --- /dev/null +++ b/analysis/lang/pl/stempel/diff.go @@ -0,0 +1,64 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +// Diff transforms the dest rune slice following the rules described +// in the diff command rune slice. +func Diff(dest, diff []rune) []rune { + if len(diff) == 0 { + return dest + } + + pos := len(dest) - 1 + if pos < 0 { + return dest + } + + for i := 0; i < len(diff)/2; i++ { + cmd := diff[2*i] + param := diff[2*i+1] + parNum := int(param - 'a' + 1) + switch cmd { + case '-': + pos = pos - parNum + 1 + case 'R': + if pos < 0 || pos >= len(dest) { + // out of bounds, just return + return dest + } + dest[pos] = param + case 'D': + o := pos + pos -= parNum - 1 + if pos < 0 || pos >= len(dest) { + // out of bounds, just return + return dest + } + dest = append(dest[:pos], dest[o+1:]...) + case 'I': + pos++ + if pos < 0 || pos > len(dest) { + // out of bounds, just return + return dest + } + + dest = append(dest, 0) + copy(dest[pos+1:], dest[pos:]) + dest[pos] = param + } + pos-- + } + return dest +} diff --git a/analysis/lang/pl/stempel/diff_test.go b/analysis/lang/pl/stempel/diff_test.go new file mode 100644 index 0000000..9812202 --- /dev/null +++ b/analysis/lang/pl/stempel/diff_test.go @@ -0,0 +1,144 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "fmt" + "reflect" + "testing" +) + +func TestDiff(t *testing.T) { + tests := []struct { + in []rune + cmd []rune + out []rune + }{ + // test delete, this command deletes N chars backwards from the current pos + // the current pos starts at the end of the string + // if you try to delete a negative number of chars or more chars than there + // are, you will get the buffer at that time + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete 1 + cmd: []rune{'D', 'a'}, + out: []rune{'h', 'e', 'l', 'l'}, + }, + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete 2 + cmd: []rune{'D', 'a' + 1}, + out: []rune{'h', 'e', 'l'}, + }, + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete 3 + cmd: []rune{'D', 'a' + 2}, + out: []rune{'h', 'e'}, + }, + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete 4 + cmd: []rune{'D', 'a' + 3}, + out: []rune{'h'}, + }, + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete 5 + cmd: []rune{'D', 'a' + 4}, + out: []rune{}, + }, + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete 6 (invalid, return buffer at that point) + cmd: []rune{'D', 'a' + 5}, + out: []rune{'h', 'e', 'l', 'l', 'o'}, + }, + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete -1 + cmd: []rune{'D', 'a' - 1}, + out: []rune{'h', 'e', 'l', 'l', 'o'}, + }, + // delete one char twice + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // delete 1, delete 1 + cmd: []rune{'D', 'a', 'D', 'a'}, + out: []rune{'h', 'e', 'l'}, + }, + // test insert + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // insert 'p' + cmd: []rune{'I', 'p'}, + out: []rune{'h', 'e', 'l', 'l', 'o', 'p'}, + }, + // insert twice + { + in: []rune{'h'}, + // insert 'l', insert 'e' + // NOTE how the cursor moves backwards, so we have to insert in reverse + cmd: []rune{'I', 'l', 'I', 'e'}, + out: []rune{'h', 'e', 'l'}, + }, + // test replace + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // replace with 'y' + cmd: []rune{'R', 'y'}, + out: []rune{'h', 'e', 'l', 'l', 'y'}, + }, + // test replace again + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // replace with 'y', then replace with 'x' + // NOTE how the cursor moves backwards as we replace + cmd: []rune{'R', 'y', 'R', 'x'}, + out: []rune{'h', 'e', 'l', 'x', 'y'}, + }, + // test skip, then replace + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // skip 1, then replace with 'y' + cmd: []rune{'-', 'a', 'R', 'y'}, + out: []rune{'h', 'e', 'l', 'y', 'o'}, + }, + // test skip 2, then replace + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // skip 1, then replace with 'y' + cmd: []rune{'-', 'a' + 1, 'R', 'y'}, + out: []rune{'h', 'e', 'y', 'l', 'o'}, + }, + // test skip 2, then replace + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + // skip 5 (too far), then replace with 'y' + // get original + cmd: []rune{'-', 'a' + 4, 'R', 'y'}, + out: []rune{'h', 'e', 'l', 'l', 'o'}, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s-'%s'", string(test.in), string(test.cmd)), func(t *testing.T) { + got := Diff(test.in, test.cmd) + if !reflect.DeepEqual(test.out, got) { + t.Errorf("expected %v, got %v", test.out, got) + } + }) + } +} diff --git a/analysis/lang/pl/stempel/file.go b/analysis/lang/pl/stempel/file.go new file mode 100644 index 0000000..dbbf798 --- /dev/null +++ b/analysis/lang/pl/stempel/file.go @@ -0,0 +1,71 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "bytes" + _ "embed" + "github.com/blevesearch/stempel/javadata" + "io" + "os" + "strings" +) + +//go:embed pl/stemmer_20000.tbl +var stempelFile []byte + +// Trie is the external interface to work with the stempel trie +type Trie interface { + GetLastOnPath([]rune) []rune +} + +// Open attempts to open a file at the specified path, and use it to +// build a Trie +func Open(path string) (Trie, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + + return buildTrieFromReader(f) +} + +// LoadTrie load trie from embed file +func LoadTrie() (Trie, error) { + return buildTrieFromReader(bytes.NewReader(stempelFile)) +} + +// buildTrieFromReader build trie from io.Reader +func buildTrieFromReader(f io.Reader) (Trie, error) { + r := javadata.NewReader(f) + method, err := r.ReadUTF() + if err != nil { + return nil, err + } + + var rv Trie + if strings.Contains(method, "M") { + rv, err = newMultiTrie(r) + if err != nil { + return nil, err + } + } else { + rv, err = newTrie(r) + if err != nil { + return nil, err + } + } + return rv, nil +} diff --git a/analysis/lang/pl/stempel/file_test.go b/analysis/lang/pl/stempel/file_test.go new file mode 100644 index 0000000..c078e8d --- /dev/null +++ b/analysis/lang/pl/stempel/file_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "bufio" + "compress/gzip" + "os" + "strings" + "testing" + + "golang.org/x/text/encoding/charmap" +) + +func TestEmpty(t *testing.T) { + trie, err := Open("pl/stemmer_20000.tbl") + if err != nil { + t.Fatal(err) + } + + buff := []rune("") + diff := trie.GetLastOnPath(buff) + if len(diff) > 0 { + t.Fatalf("expected empty diff, got %v", diff) + } + buff = Diff(buff, diff) + if len(buff) > 0 { + t.Fatalf("expected empty buff, got %v", buff) + } +} + +// TestStem only tests that we can successfully stem everything in the +// dictionary without crashing. It does not attempt to assert correct output. +func TestStem(t *testing.T) { + trie, err := Open("pl/stemmer_20000.tbl") + if err != nil { + t.Fatal(err) + } + + wordFileGz, err := os.Open("pl/pl_PL.dic.gz") + if err != nil { + t.Fatal(err) + } + defer func() { + cerr := wordFileGz.Close() + if cerr != nil { + t.Fatal(cerr) + } + }() + + wordFile, err := gzip.NewReader(wordFileGz) + if err != nil { + t.Fatal(err) + } + defer func() { + cerr := wordFile.Close() + if cerr != nil { + t.Fatal(cerr) + } + }() + + cr := charmap.ISO8859_2.NewDecoder().Reader(wordFile) + + scanner := bufio.NewScanner(cr) + for scanner.Scan() { + before := scanner.Text() + hasSlash := strings.Index(before, "/") + if hasSlash > 0 { + before = before[0:hasSlash] + } + buff := []rune(before) + diff := trie.GetLastOnPath(buff) + _ = Diff(buff, diff) + } + + if err := scanner.Err(); err != nil { + t.Fatal(err) + } +} diff --git a/analysis/lang/pl/stempel/fuzz.go b/analysis/lang/pl/stempel/fuzz.go new file mode 100644 index 0000000..d9721bb --- /dev/null +++ b/analysis/lang/pl/stempel/fuzz.go @@ -0,0 +1,35 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build gofuzz +// +build gofuzz + +package stempel + +var fuzzTrie Trie + +func init() { + var err error + fuzzTrie, err = Open("pl/stemmer_20000.tbl") + if err != nil { + panic(err) + } +} + +func Fuzz(data []byte) int { + inRunes := []rune(string(data)) + diff := fuzzTrie.GetLastOnPath(inRunes) + _ = Diff(inRunes, diff) + return 1 +} diff --git a/analysis/lang/pl/stempel/javadata/README.md b/analysis/lang/pl/stempel/javadata/README.md new file mode 100644 index 0000000..7c40b7b --- /dev/null +++ b/analysis/lang/pl/stempel/javadata/README.md @@ -0,0 +1,3 @@ +# javadata + +Go library to read data written with java.io.DataOutput diff --git a/analysis/lang/pl/stempel/javadata/fuzz.go b/analysis/lang/pl/stempel/javadata/fuzz.go new file mode 100644 index 0000000..5ad4c21 --- /dev/null +++ b/analysis/lang/pl/stempel/javadata/fuzz.go @@ -0,0 +1,34 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build gofuzz +// +build gofuzz + +package javadata + +import "bytes" + +func Fuzz(data []byte) int { + br := bytes.NewReader(data) + jdr := NewReader(br) + + var err error + for err == nil { + _, err = jdr.ReadUTF() + } + if err != nil { + return 0 + } + return 1 +} diff --git a/analysis/lang/pl/stempel/javadata/input.go b/analysis/lang/pl/stempel/javadata/input.go new file mode 100644 index 0000000..0da4d4e --- /dev/null +++ b/analysis/lang/pl/stempel/javadata/input.go @@ -0,0 +1,135 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package javadata + +import ( + "bufio" + "encoding/binary" + "fmt" + "io" +) + +// ErrMalformedInput returned when malformed input is encountered +var ErrMalformedInput = fmt.Errorf("malformed input") + +// Reader knows how to read java serialized data +type Reader struct { + r *bufio.Reader +} + +// NewReader creates a new java data input reader +func NewReader(r io.Reader) *Reader { + return &Reader{r: bufio.NewReader(r)} +} + +// ReadBool attempts to reads a bool from the stream +func (r *Reader) ReadBool() (bool, error) { + b, err := r.r.ReadByte() + if err != nil { + return false, err + } + return b != 0, nil +} + +// ReadInt32 attempts to reads a signed 32-bit integer from the stream +func (r *Reader) ReadInt32() (rv int32, err error) { + err = binary.Read(r.r, binary.BigEndian, &rv) + return +} + +// ReadUint16 attempts to reads a unsigned 16-bit integer from the stream +func (r *Reader) ReadUint16() (rv uint16, err error) { + err = binary.Read(r.r, binary.BigEndian, &rv) + return +} + +// ReadCharAsRune attempts to read a java two byte char and return it as a rune +func (r *Reader) ReadCharAsRune() (rv rune, err error) { + var char uint16 + err = binary.Read(r.r, binary.BigEndian, &char) + rv = rune(char) + return +} + +// ReadUTF attempts to reads a UTF-encoded string from the stream +// this method follows the specific alternate encoding desribed here: +// https://docs.oracle.com/javase/7/docs/api/java/io/DataInput.html +func (r *Reader) ReadUTF() (string, error) { + utfLen, err := r.ReadUint16() + if err != nil { + return "", err + } + bytes := make([]byte, utfLen) + runes := make([]rune, utfLen) + _, err = io.ReadFull(r.r, bytes) + if err != nil { + return "", err + } + + var count uint16 + var runeCount uint16 + + // handle simple case of all ascii + for count < utfLen { + c := bytes[count] + if bytes[count] > 127 { + break + } + count++ + runes[runeCount] = rune(c) + runeCount++ + } + + // handle rest + for count < utfLen { + c := bytes[count] + switch bytes[count] >> 4 { + case 0, 1, 2, 3, 4, 5, 6, 7, 8: + /* 0xxxxxxx*/ + count++ + runes[runeCount] = rune(c) + runeCount++ + case 12, 13: + /* 110x xxxx 10xx xxxx*/ + count += 2 + if count > utfLen { + return "", ErrMalformedInput + } + char2 := rune(bytes[count-1]) + if (char2 & 0xC0) != 0x80 { + return "", ErrMalformedInput + } + runes[runeCount] = (rune(c)&0x1F)<<6 | char2&0x3F + runeCount++ + case 14: + /* 1110 xxxx 10xx xxxx 10xx xxxx */ + count += 3 + if count > utfLen { + return "", ErrMalformedInput + } + char2 := rune(bytes[count-2]) + char3 := rune(bytes[count-1]) + if ((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80) { + return "", ErrMalformedInput + } + runes[runeCount] = ((rune(c)&0x0F)<<12 | (char2&0x3F)<<6 | (char3&0x3F)<<0) + runeCount++ + default: + /* 10xx xxxx, 1111 xxxx */ + return "", ErrMalformedInput + } + } + return string(runes[0:runeCount]), nil +} diff --git a/analysis/lang/pl/stempel/javadata/input_test.go b/analysis/lang/pl/stempel/javadata/input_test.go new file mode 100644 index 0000000..b4458c6 --- /dev/null +++ b/analysis/lang/pl/stempel/javadata/input_test.go @@ -0,0 +1,249 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package javadata + +import ( + "bytes" + "io" + "testing" +) + +func TestReadBool(t *testing.T) { + + tests := []struct { + in []byte + out bool + err error + }{ + { + in: []byte{0}, + out: false, + }, + { + in: []byte{1}, + out: true, + }, + { + in: []byte{27}, + out: true, + }, + { + in: []byte{}, + err: io.EOF, + }, + } + + for _, test := range tests { + t.Run(string(test.in), func(t *testing.T) { + sr := bytes.NewReader(test.in) + dr := NewReader(sr) + actual, err := dr.ReadBool() + if err != test.err { + t.Error(err) + } + if actual != test.out { + t.Errorf("expected %t, got %t", test.out, actual) + } + }) + } +} + +func TestReadUint16(t *testing.T) { + + tests := []struct { + in []byte + out uint16 + err error + }{ + { + in: []byte{0, 0}, + out: 0, + }, + { + in: []byte{0, 1}, + out: 1, + }, + { + in: []byte{1, 0}, + out: 256, + }, + { + in: []byte{}, + err: io.EOF, + }, + } + + for _, test := range tests { + t.Run(string(test.in), func(t *testing.T) { + sr := bytes.NewReader(test.in) + dr := NewReader(sr) + actual, err := dr.ReadUint16() + if err != test.err { + t.Error(err) + } + if actual != test.out { + t.Errorf("expected %d, got %d", test.out, actual) + } + }) + } +} + +func TestReadInt32(t *testing.T) { + + tests := []struct { + in []byte + out int32 + err error + }{ + { + in: []byte{0, 0, 0, 0}, + out: 0, + }, + { + in: []byte{0, 0, 0, 1}, + out: 1, + }, + { + in: []byte{0, 0, 1, 0}, + out: 256, + }, + { + in: []byte{0, 1, 0, 0}, + out: 65536, + }, + { + in: []byte{}, + err: io.EOF, + }, + } + + for _, test := range tests { + t.Run(string(test.in), func(t *testing.T) { + sr := bytes.NewReader(test.in) + dr := NewReader(sr) + actual, err := dr.ReadInt32() + if err != test.err { + t.Error(err) + } + if actual != test.out { + t.Errorf("expected %d, got %d", test.out, actual) + } + }) + } +} + +func TestReadUTF(t *testing.T) { + + tests := []struct { + in []byte + out string + err error + }{ + { + in: []byte{0, 3, 'c', 'a', 't'}, + out: "cat", + }, + { + in: []byte{0, 2, 0xc2, 0xa3}, + out: "£", + }, + { + in: []byte{0, 3, 0xe3, 0x85, 0x85}, + out: "ㅅ", + }, + { + in: []byte{0, 6, 0xe3, 0x85, 0x85, 'c', 'a', 't'}, + out: "ㅅcat", + }, + { + in: []byte{}, + err: io.EOF, + }, + { + in: []byte{0, 3}, + err: io.EOF, + }, + { + in: []byte{0, 1, 0xc2}, + err: ErrMalformedInput, + }, + { + in: []byte{0, 2, 0xc2, 0xc3}, + err: ErrMalformedInput, + }, + { + in: []byte{0, 2, 0xe3, 0x85}, + err: ErrMalformedInput, + }, + { + in: []byte{0, 3, 0xe3, 0xc5, 0x85}, + err: ErrMalformedInput, + }, + { + in: []byte{0, 1, 0xff}, + err: ErrMalformedInput, + }, + { + in: []byte{0x0, 0x05, 0x44, 0x61, 0x52, 0xc4, 0x87}, + out: "DaRć", + }, + } + + for _, test := range tests { + t.Run(string(test.in), func(t *testing.T) { + sr := bytes.NewReader(test.in) + dr := NewReader(sr) + actual, err := dr.ReadUTF() + if err != test.err { + t.Error(err) + } + if actual != test.out { + t.Errorf("expected %s, got %s", test.out, actual) + } + }) + } + +} + +// func TestFile(t *testing.T) { +// f, err := os.Open("stemmer_20000.tbl") +// if err != nil { +// t.Fatal(err) +// } +// r := NewReader(f) +// reversed, err := r.ReadBool() +// if err != nil { +// t.Fatal(err) +// } +// log.Printf("reversed: %t", reversed) +// root, err := r.ReadInt32() +// if err != nil { +// t.Fatal(err) +// } +// log.Printf("root: %d", root) +// n, err := r.ReadInt32() +// if err != nil { +// t.Fatal(err) +// } +// log.Printf("n is %d", n) +// // for n > 0 { +// // utf, err := r.ReadUTF() +// // if err != nil { +// // t.Error(err) +// // } +// // log.Printf("read: %s", utf) +// // n-- +// // } +// } diff --git a/analysis/lang/pl/stempel/multi_trie.go b/analysis/lang/pl/stempel/multi_trie.go new file mode 100644 index 0000000..32283ff --- /dev/null +++ b/analysis/lang/pl/stempel/multi_trie.go @@ -0,0 +1,140 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "fmt" + + "github.com/blevesearch/stempel/javadata" +) + +// multiTrie represents a trie of tries. When using the multiTrie, each trie +// is consulted consecutively to find commands to perform on the input. Thus +// a multiTrie with seven tries might have up to seven groups of commands to +// perform on the input. +type multiTrie struct { + tries []*trie + by int32 + forward bool +} + +func newMultiTrie(r *javadata.Reader) (rv *multiTrie, err error) { + rv = &multiTrie{} + rv.forward, err = r.ReadBool() + if err != nil { + return nil, err + } + rv.by, err = r.ReadInt32() + if err != nil { + return nil, err + } + nTries, err := r.ReadInt32() + if err != nil { + return nil, err + } + for nTries > 0 { + trie, err := newTrie(r) + if err != nil { + return nil, err + } + rv.tries = append(rv.tries, trie) + nTries-- + } + return rv, nil +} + +const eom = rune('*') + +func (t *multiTrie) GetLastOnPath(key []rune) []rune { + var rv []rune + lastKey := key + p := make([][]rune, len(t.tries)) + lastR := ' ' + for i := 0; i < len(t.tries); i++ { + r := t.tries[i].GetLastOnPath(lastKey) + if len(r) == 0 || len(r) == 1 && r[0] == eom { + return rv + } + if cannotFollow(lastR, r[0]) { + return rv + } + lastR = r[len(r)-2] + p[i] = r + if p[i][0] == '-' { + if i > 0 { + var err error + key, err = t.skip(key, lengthPP(p[i-1])) + if err != nil { + return rv + } + } + var err error + key, err = t.skip(key, lengthPP(p[i])) + if err != nil { + return rv + } + } + rv = append(rv, r...) + if len(key) != 0 { + lastKey = key + } + } + return rv +} + +func cannotFollow(after, goes rune) bool { + switch after { + case '-', 'D': + return after == goes + } + return false +} + +var errIndexOutOfBounds = fmt.Errorf("index out of bounds") + +func (t *multiTrie) skip(in []rune, count int) ([]rune, error) { + if count > len(in) { + return nil, errIndexOutOfBounds + } + if t.forward { + return in[count:], nil + } + return in[0 : len(in)-count], nil +} + +func lengthPP(cmd []rune) int { + rv := 0 + for i := 0; i < len(cmd); i++ { + switch cmd[i] { + case '-', 'D': + i++ + rv += int(cmd[i] - rune('a') + 1) + case 'R': + i++ + rv++ + fallthrough + case 'I': + } + } + return rv +} + +func (t *multiTrie) String() string { + rv := "" + for i, trie := range t.tries { + rv += fmt.Sprintf("trie %d\n\n %v\n--------\n", i, trie) + } + return rv +} diff --git a/analysis/lang/pl/stempel/pl/pl_PL.dic.gz b/analysis/lang/pl/stempel/pl/pl_PL.dic.gz new file mode 100644 index 0000000000000000000000000000000000000000..150ec950ebbb28edf91545a6af4c1a8823edd118 GIT binary patch literal 1205814 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�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

}>b^{9&OIl{aSE%cQzO8yf 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%0DapZ_exzoI31lb7}hKRkpw}bZ*9`=h>mP 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

e~|Xh6LfS12r7>( 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

^X4Zp%=Pt{F4ga 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

O8`e(2eW?DS}maI`ShFjQL2G z$hlM5dIUTDQ{0R}2*<4n%3cQ|T5}aq#)>1~gBYXkRmPUFvMsQZS<7X5jbT95$uIoN zSVIIcJ%A1u*ZGcz2LDWPdqEi4M*@r`>i|bhc*TN%<@}+M4Pp@W4Ko0Ck_VA>C|Du?Ji4B4` zh_!&7GIh$0Uxhm$C?2JN15lAYJ?UK+5X0drB*IvM7%c)PIT}Bnaz>1lDOs%XH{qYI zIn=Q+K7nc>8Vc+ReGFJ}07|k63-aeWNbQ8TXZ$#y2`&8qTlLT#A00tIkgE^~gF{G| zLjZk)2gEQ@%e4PR37;w(q=h9S!C6Ct7_|LxzfH{GABt4!;^p_FM>-N`Dg=475jbJI z@^o=1np+2gNE=>6u2p1g4I>j%nS`-!zxZ4N-y>Fx!7o@TzZo{G}qGvxd zpc@My6oGQuvEjHg9&H>?cbGLdC;Xuv4a94ct?c#S9jcc zxS>KAC(uX4*jF;nX4$ET%Gko1MSUg^pA4c?5@Vh4&#P(>$pFp;YFko^3KJA-R4@*b z);iir;6WGcRSxnih3|1HG6o2@$|zVnN5(o(Wx~=@mKsM`H{^cdvD>3<+2F2q=W%x> z`BhTnwU(CuCCl{OMIy;HG_SAf;%jV8UVU4V!Dg-3Yfzw*VXL0HIJn1s#`908jJu*A z(@)08wH#-7`@v4+5TO+XKqorW7!@CPJnur~-|p2$QU+eUHO-_8C}|9L(NHGY;KsN( zh6ubS9<%AELLvFaMD)uI5zj$=9m6;Y$^)0@+ig#>7(O{0?f6glLMg>U69T^e;vlQ+{P*Q9xZ?EGin?djI`lTO~@T zy^Dx0>Za)rC8 zT|?6Fio#oEgUhCpcS}Y;nQxxE=qC2jNX)+K6ApD9DIJkBa!s8w8c1bAq>R-RWomy4 zfv@U96!lwZuHpo-!D6w9fzFcw)$&`9&2aWUdHD+;{p$~8|1WGJ;dt#-l_7y1Jv-A% zLgQMF#)@%s&dqUGvxL7F=y5S{)Cj_}a@P4bL&C{(o+IMhBq+e6BqM5{rk^w0fRjG- zyO7YFxY9J?NESmKFv0}%g?S5hco=Y3UB-wXRt?s;`_yPm^j#9XTm)7|gTY#=udFsN_Q};g8IkNmk zjejaCDj@LkAc{~PQE)|tvDSIOnvo8WiL{a+!@ea+I2i#!j25!)Cnqmo6R3rSrFWHR zj>xi$b{*r5FF#NFT;1k@mf4WfwbP7>VHqCzc-8;E9uoqM|v*V~Ywg3>jis36p40+gQ8qK!Neg5nQvpRN^-UBW-w z-gH*<;pM_f3t;DuHfNONt)T3(Q`|dtunu*DY{EUF@s_(odB-Ay>2Z~Ed`lmi05>Wg z?nlg6p^SzdnK0lG#xGJa*OiQvM?ytY ztfcH~DO0$@jWYomi$FB=%}0{OFSst*>Aue3)I~9}A~jUOyZRXOln6@8a}(hcafT6BmE*sFd;qB;4G!<0gZFo-U3SGdp?&z4_c+CUnSr0e~~>s#7uQ(iIjoH#BYh>0~q#Ar!{$GHtqbN!63i}r2v}o`oqitl z14VVViZnldCg9L|!)G!_DVTZG4gZl!Wd3A|7<9U8rMFTWe+78Y1 zrc4zl>VfqRF&pNwOj}`s_P2rq`4*y69)|jEjv47c33?|1>y(9{yB1-LHOfE@$LIz> zJdpmWSW7veOrMmJov1YVz8rMm+83-FS^h1d*9uVUOL? zj+l_4uVWpYdD2%tMiuk`2go;u;E`;WMQ%;+bIghAA zAlt6mEl7YuwA-=fr8BlL?(iChORILRT%5XUuyPLXG@OgYL1SH>y9XTz`ABEzicm(I zrT_yUTcASr6X9K$@_4(cd{^ekB1^Aj9$!OY%_w>{N5958*0GHJaZY2r7f7|PDmn%9 zj6xp4Qx(p5eriKtvFyzP4eHKzAFBDOO3cuQ`l7q0lU#SpE$M2HrHu2Y?+a#mmNtKZ z+5(-(;@<1aXpBb^nb2`HLGGZ7_l^iVtE%gD{H3Wu>EgJ$f%aN=w$%56FqjqdH&6Y` zS<%|HowKbC>Nu^tZn&<-d&dTB^O-s4KohiW5^uIh`su(LzR-Jp>iTFkI*q zxt$XtAWK=o0W-;#M}#dw0lUqt48QT{Zm<1}b+7S^qDhq#@r0Iu-dU=9CvyT3O3tJR z4^WGUKUnaazKnMt>fOB`z}VUY#;yqf235(i^sZ8E5)OV?zL?lADw~DPBux!cE~g@; zOVIj<_`*e_)l04fyGDKwn@MzUM^Udv-azRa|5WlTPVsFj5kEv!P8j{b?3?vks9KmL zzxo|jMUeFO;3NyS-mfCy4V3l=mf`ABXpa=Dq1Mv9HWgd|_?Enhz=nbQUWd=8II|)` zSA+oCH8ux#G33gK=XXJKrUDwO1Mue@2)EW&OjLlaaZth~$7H4aU+jZunMYt#@V)*X z?+!B~WszmMa!rig+#$@Z?EaWs+CJP}R79@oKPGoI8=*NNJHjCPiu!O@SaX!{0U>^Q z_4j|^z{mr9w+1-X1(QS}uZZXF6r~)3{O@mB zDBgjb^cl#h3au1CcbXd^yQOnkmSDD;#drMcA0tCZO$Zo0kdbC`!YjMdiW0IH(ium~ zyA?SmxJ0}l^!|pJ@Gd#nN{DU7nE^$KmXW67mhhxkgi(?jfM9fi17yWy3U`TL5Y|j< z8bX5)I%`R!PbuQx<&J&+lB!GoZ7%*$782%v>(%4KSC$g8ABlT$`ESoTmgEh!Oo6VC z7;B<9;skCMJ0c_|h^#U)VK@AQkc!9HsvZ+mX(qg8tt$vqH7%s^&PT>msx0 zMitGR~xTaVEQpGucg?NiX6|dJku^YdBN5cJuPL z-~JIexa0!An}58Lpj%Q9oslzW6~h@Y+VGYrQw{*bmZ$Np$BKia@^ zGTrK2r|fwb7{gC)=Ky6GW1(QlQpa5mCS+vBkS>rD_~E;1U~H&PFcxm1v|<1;K>Z}M zs0L4|5CE;I3`+OiyKeLW>DNMW;~jwvPF*Z2KyZF#vG!-(h4GmU3|C>1WkgQ|mzFI0 z=MjjoBs|%3PK2ZYWEs_~v1Iio&t;Kqi{NW%t6QxmHJ!?zMAto1u%R=dn)^lsT+aY; z!5=yU51(}n{~R_IX!0na7XwEtS>$~eGQQ*8H57H|j9W+W_rQwLkb*`pSAsdB%7&1R z)kfKIF5j5T6*`@YEcHg~(m_u^O9qA64o{GR^nY3bMf_n$J{q6FjbZrO@KT?K|J&uZ z=|tDjmtVJ_?9Mf8-mDXSpjJHpln(z_G_Ler=Y-T)2FD9=jDk?B?I-d|9^yK+EH`d|7i|tI{t6N4Sx5p3knA=XZWuq z4fuBRe>o89$KZ6{2~NlT5ft00!BgEp_RDgOCSMe3>3Ge zcS*L=El^9g5KDkuqVu>@1)q8Vn=VEmb20Id2uTOSX!l7u3h*Obc!lFwbY`RIU7q;K z3-Jm-GRX%kSNhVGi2B_J$s;B8xV*RVY(411s3 zcc-%qp_hx~1!|$XlSOUQ5!tFcAWgO`(`B8P|6xSy4~b+QFe~~O8C_-DlEL;AlAt}D z2>syhm+q0Ur9eQk4oWh3yEEF3^t#W>7W@YS0)fkMugW!gV@z> zsi(bFr?$vZro^R$fAEQqen45y#(-$&Fc1x9wDgB@3yZp~W!C5D!tg22>k-fr^EnnF z`y5@k#&qz@6o{qaL*Ohr&oYvdkj1hb@QEf03T$~*)C!?2IgTp+tdIkwSsRO5kuu(j z87ET`0svvA$drVAIfMjQELMv~V~MeNnc%2<{u9+(GoeM#ojm|oZq>o*@+9cPjRbip zBC2>oV3Pr<2h~+gV=;}8bZmM^9x}}p=$}B(VXAi{*3ANuZ zHz~C@)hZ7nt+<2$3CV$C=r&1&QrCx?e01Bcgn0D@WLPR|#MV>=5ebPB3mKK%>{Kdxf!q?F?j>xM6_NR{=*zw+N4;c7Ny`s%^z8V? ztXZjiX0ch{!P2cr znRK%~0tD`r4KQ}PGcy|NV|{h|dBB{Put}(t-xustJT^S_Vc6e?6xlT?KRfzMZ5Ii5 zlCVp`-#di-(?{~NyOaGJ>zA1NCCpU;N}Lz}3I1xMGYdY! znQ~tThd0}t?l&DK!J$5G;y|>m?Tjpy;a-X*q)iC3>|UM9;3A{6EIKf8F0S!}5HJ)`8hiFAfr1OE$B1lS9HAAVbP$E&Bqq03M})cKwTlz9 zsbxCc(wT~TI=~@3_-X9m1fZju`$H)NwE+fb%ZC{y{`tVJ8YK;p7;^Yw2@lHI79j2onr)LP-_pkFVL{(!xJ%Z4&`Ra|GaU?H ze%dqt&<14wp^c#|aO#twQ#3_EvhBAH7F*iIPMv{bp_utv&SSP+LQ34C(iS zpAlO-68M|0zx(o6{*_X3)ss?5fOWg8oRLbuF<8FBX{iKaD_ud6&R0z3t95Y{Q-n=F^$+=uD_CbT?lQ!r_`;rv z+ndE>b|f-{(G!ozi{v{Ty z7CgC8pyU@BF7j+6i=zqDg-cLWAOB4cu_BnqUrWq1EP1A3iKzv196T@W^N_VaW&HZ( z+SfM-mG98eilS=5YH0`gdlV&wq+}~fgtxZ@cAPI2B9gitV#5=o4K&OpZ_rn44j2Ps zaFxrTLKOup_M#9zY!$-1Ye(AuMJFM7yI(lms>e9l^A*B3ktcX zr}_?`>AQy~$YoK)D-G5Z=c3%Eeer~8G@YY*fe^EqBN1EaJeN)%9w2tt#`m@?8V`3YYl&R>(R&jitCju4zSmY?O_#tK~ zI?biMl?44eNuLkZ@-tP-PqzatA{NEZtD1=~c^8&oUmPMNd&pUI&n|?ZS{5)0$*OGm zO}Pl5gT2vO@2M76NB9;`GJ&M5;M)@R< z$99U!Ld4*4RtMG0SJV4Ta#Afgttu_a12sd}zn1Bp|4LDUwV5J&nJXfM{`rwIM=&N_ zVZusLP`V7&g3S@i-{%ohf>4Z&0z(kvb5Uojk5mr6lSXw{{3IrF*vFzpVZg^0=2{8w z-S`}L6RIwMC*MC$VwKF?DblL6K0p00CvX`Fiw|{`tGE+`F+aIu2SrwS2~5@rTF8rx z)l_EHRb)3`7RVzr5jP7A=dy&@Fl?wQj4+Z>`-&5AZxp)@&0*`VUNo)5HNH}V3iOUrIJYuW_rPU)&3|K zT?H{`Yp=%pcTnL9T+fgccRb64AH94Y+Jfpd z@lok@Ex6v><#tE`GxdYJ9&f7OK8`el$lXU3HLVhrX8%NclcQLCS~nAZl+{ZJ?LjE5z^1xeQS-vQ$A;5cOi|{l9?nQ8{<4K$%dKvq2N^P~wU#0&O?wNrcjUSo%SoKjq9|owvc;2w zGgIDemYZ+DcrtB}HJe(~M&&4!Z0q_X5;u4v5*+K!zCj{F?OZEEBPKKi6}4ckd2cUc zdv7{qj$vG>>-Xw0^mW>z=swY2zE?o8Oo=zAam_UR<_% zigEJ^#xH-RK4~={F<)j~*x`wkHMwe;*R-Gjbb9>Ggd2Z&2w#-rj&$ILaQ#m!IeHNT5e76juCFL zF|^z_KE7eZH^`c=Rj|L!ne(q!4*MOYG=**>7UqJSe*@DgIdadV;wSFW;z)`l;kXio zu^@O&UQ%IN{Aj=IcwgtAmG;@8}_E33)I**k~10zp@8MPYV6fJGpDc zyj03!Wx%`7o!5}^3z&g7d`lns4ZreR{!EGSl=6?UKRsRj*{RM4a8#uyRdRuUWt%7> z!a9tQg?T`4Mo!?>s-Q6kFKVse$pM(|H%2j(Y9S!#E4nf2 zZV-nR+e1c{0}SdcC}enDD|+o?12tj;H9j^_<9)oO6k;NUSuj>oCM=2se7b^>ji&U^x79JBUsgnjb3y5w2wUE=W!65`mhr-9Xylj~yAQ_p$rGE5 zN8p5E-xFiIcNr&W&jr=}*7_XU;GVT(g1Dj~7^aAshGJ|MoR|xD@Z_E%z!?@+m7>w=RMUL<{Hd0`-9*fB)N7iv`y|D#e`FWS0R)5&Y zoP;<1<%y>vZ2H4?StMNSSKcF$_$mtpbX=DW&5P`5PFpTw*@ch|O-cUnhszLW4r|!f74^Y+3HB`lR`r z+%r_4+ztN8-H^VpL*;{^y_|L5zi9+yD{&F_QyO72)AKe67Rmjh3i9zPUO@ipa>H4~_4H&xEh^kMwV}PmC{2 z9~j@4KQDdVJzW6Wv?xOIX`W5^W$D8%;8lo`Hvt!G@Qc)@T^T}1YMAJ?=pvp<#A5`W(VxdzmtmTtg7?f^NE*ScfxOW)fFw)tv7t zOvc%T1X1KY;{TwNqliSNudDn|7O`mENztefVp`sAmXxzZ}v58acssZd+>9oJG& z-fF7){Ok02cz9p!m`BM=()Nfe4M=s{M7C#9kftV<`2;@%G9CA}ZmQTjiU_=KRK!&U z)F2hhc+}92IX;13Y{sMh?w`l}*12v}Yqv1MO>6!DW*%$Yd!`lc!kq*o8ns#p4S zVU~yPWx0=4c0KerOI9K#XMzzE8_@kC~9S&8oy0y~`-$Agt;qz3b?TuZU zRm>Eoh{4o%(N@_h?2z!MP&e|QyOfVv73qN|VydOShYc^LakZTs0* z+1>D!jz9c*MNP=Ieo8E z@@4@(-x2sB3`_`RhR2(9rQD!?YC*R9mSu5iZZc0Zs0=d_%8Vh|_Y2hTw>zOkQz-gV zeYwu^d|^?(T6s(YMiDQCIciFd2p};E^UWuC8FwlDY!*pmD02laJxtpwQGLeJd;XD^ zDvbNm=g2FrV!!nH90lQU-x&B0WU6)I|CP?12M|y(Vf96?wr?iWm=HgpvS=K zPvJD5Q&2L7oX$gh0B=<+Z4dolqQ6vr)))7)&XSu`EX^s+tO}OQV|mqwPW7B>>5aO! zuO3#)o0{qO0p}{H{tQ7~s*9Htr%S%m-g&9#BbR0n zQ;4Nrx>|6hYJr8SrCz7fo2#7q7FqQzwM>F7-09$Pp3qMML%#`9Wx5;b%IvlRvJp5`o9Y5tj~`xxup*YJ+Rz~@~S@X5R>wAbT=;DRoi1$p6O zFr+Xv2s$~+Mu!WYE%jyTW>3U8Ms~r|Vwd}Rh%_u9y1ast*ICB$srPz5n;0!3cbnNR zlCFVb5KQ59eBW2}+Nbzt!G6V_LNFv2W4-tC2}X4V(P8H0*VOyv!uBDl=?%`Cw+mR2 zNVDpc*)=htRe7X;MWI#0{V&Vtj+@ExrjO19M4w356&ePfAY*hOEQcF4{$!y-^Hc%@ zwyYT0rH@uR0T`dq)d^vv;c zc>zZV&7KLDZx~AN%p)#dzF6phtRE+7bs;57zUHn~mN8lY-QJdV#VysIwTW^Egfc0^=L{PR2gcbPVZGJG9jr*B0ly0 z3{RcKB$dR~Z%=j~<@N2wT*(#i>2Bpn?qaY)Ft*7Mio`5HJR7p4b~z+h-3T$|TF;^v z@*;YtI*qO?($0ePWurI#6L?1{Ao{T>o0c)w?_TZMy_wyKy<_-H#&~mDn-R(qbAJaH zQ$ITcSJ4R17!Uk?2Gw4071$hM&G&drL$w$Y5rLL!&6my!Y1u%9=M6+Hhz>7tFpiw(8ivz|lGh^NtGn%W z1TemYL0e=TlM90F*&(!!HPzqMYwv3Hb**dbip5}+qs>aAK9Svp{5CF-(S(W@zs#YC zba6?NljYIbRfONLlS`7(6zOL)QQyZ$uxYeDJavx#3#bj}!mAnuRb?St49 zfrZh2{W(crlKl4>q=>dJSPQ@lPA~c)WX{6Bt^J-xlip0YXnHPo$tw?m%`$7T4-*>~ zk-R~^u{ZPR$rxi1W^2_qb~j_Z_S|qE@fU_t++a5`Efhu7D*#pW zTUrEBsDmT{`R0tmIgnwk9XC6!mzBzDLM1toQ>2wiK&6yza&G;VMgR2eEx-JwT{S-9upYf;v{vQ(N?%3L|PY)_I9abSGsZ*Y;%UqJ~fS+vy zMny`?x{v&)WW>+S((y0VCp$2lz;q!?#3DV9boRIfOT=1AHxh@QDibotsr$KgsdMpW z;`;oY9yoV>&jJOdBV|U@k!dXrMVeomDLp!Odusyq^yPJQ-s7#If3ZV>WT~FF?3w1~ zd+drdK7W4;vqU1UTbzbv*wT~mJ~TJC*>++tl}0!B04FP;M!1C{5g%art$5(B-2l8( z92(r{T*;VHX6W%g^B-;w8TYoNf1}0iLnKO{8Y;qL+jsf+^jm{T&|}i|sCC?$i}VgG zP@llUAdND~dTqPtuRq8~Ir9(o6)aE>!864AbCKUse>wEQs-)CNB}}_kD9z=K{Ak_6 z3GD6B2W+9%s&#Y90;yk!FDPpF?VbLSaL~D6<^D)1{u<;qNV9k-yZB^Y2A}I=<1ZM`2^;?Kt8LY0hz)oI+B+6|DV*gm2RDF?ke~lk;fhwPB2~4jJ8*~xcX-TW zWEo@Lx+6P3vUC;b12u9R;6CcLO9@m8>iiZ*G`KQpDm7v{^#wkvDtd&8L$|p5XBiasG#prTUmJjB#aZz+v+Z z3wTGGefj(TJMoiXJ`(u%?RV}Qq5N+0mz(5HTJ26$cdw%zm6%>PeM^4#`NTcO@~yp{ z#D43)n{?;1G2`yL9gay-`aH%rdy36W;VCmRC3g*iuLDcb-@V1kNea6rO^j!x+`;O%Whw z2WF(s)%tLOPJ|~X*?zj&Myh# zw5ecx8!X{49PKya4Rd~kvgTC?Qm*j!q->3cFaaq*+c@xCc%xOP8B^p4iOvRIo*l=b z%MniYU$^Aku%Lc?hPk5K*f(chS(dSxwtK+3do&;;?na>zldumD!wShQEe_qr%X$tM zi#J@#xKa1q{vd)t!%KpVC_48oR|DiDr>Noi{_(pn|NB&g>C;?XkQmi?+Q;KwL2MnA z(5fv6lo2N4_<|cA#!JABO#;xO2P1#S3>A+K6_e61yRFv^59lhurT}d|gA)P1e3mXc zJoZb-J8Q_vEB~!>$bX{=#^?#we6+P-O^Q`dxxPv3U;eVX#eIPE0Ojuo|HuW2{x1#9 z(_1$G()P{KUH|^#8q6;*cI0v&PDgJC=X zwy^>wV3E9K7Q&ilMesF8@H5ZATcrKy{>Meer>4r)tup$(Q&Qp;Tg<1V#tV+BqZoGs zDkVDr1K5y37@0@<`yWaQwv9{y(-v;}Yek(5OL>c0skq@wc<q5boWlAMEru$Cye?NFtfYwkv{q*cR@LXB`u+1TcgpBgUG5#WK=Q{LlljlFx= z+{be}xw=-;rHOITPUmmwbbFDbs~}ls*+_(kyB)e$!6~?IuhKv%{}{p={c%uTV0Z7$ zlx?(O24~-hn_t?=l1pKIOX&}rureX0j)+d`>|>lKy6lQUDcoV}!WW9#pP92r$IMib zy)N+DNLn^eoiScgafRgzA@Lc%kKtZWfOqpdt^n-gvSAX$h3}Ka8EiiGHJiQ) z)^fJG;VX-IN@LFkBD>j#>}npe^WFd6zP_;>`DE)P`!;Pe_iY@o2c+&PzWn{~|Mk1^ z*NJTJMFq z{PNL=+e-dz`M%npq%o#M3)V$61yEd!F~b7EtqQCZvxKvwK>XsDApkKz`~chBkM=ox zy->oFMW7}F11TpW=Ymp3BHqA!w79JP;htnZNJ;tl^xYO8_mNl5rR!xBc|b^t&aq_X27X9!_wVXw02|jL}PpGQw5!vLt43ds| z$mhp)vICeuHohW;Dj#O1@*zelA8MlVAqFbpeV|Be*3*-C4Znl}A}S6eHdu>3%*jg; zLcJ^i4(nuu6=pN7*Cf{G2%v*5!jPgHnDP*a>Pbk`db^F`B44-qd_U~J`+of0_qqJv znu8J+*{q4B)P5^Nky26L#E1*v_xaM8IqYp`Sd?V?u_@*vS6gC%@kppvQ$qM!#(Cdw z<$s>y&|0I&h8N#umj3&#WaagDPrti1zk4eG|GxbH`vPJ{>rnV+;!d>)KZ}Waxr4Oh zlj&UyFV+t~5z;Fn`_~E7w&JQF>b)}>X~U=%0;k`ufetQ+o-HWd<0mZ(RMk(wXlE@e zlp_ue#AEJrj{r<86!?}-Lo9?_^p{9`+K9UoCT)V@npDrLDxOc}_YC(A)`c?=<%PxE zJ_n`Wt^~BNPsscdH)yi+Wo}~W%0k(FXo>ZFB;vx%Mdaq}>)yn|u#69|qCZw3MB+kp zcs<0J%HWLU!6+{fO6WMvu+6Aa=xzU8YJR+&HX!qb-u$=`JG>w4SDP9^>bv4+egaN> z=c^rLSV70TM}keQ^duU2GCz3s?0PLapBG9fOovyuo@dafSC8(6MJ~d^q!C*tL{pT! zD)93FNk$O9dK9-PBBpN~`!0uCK`245cJTaS@6-c$pQ=-v1g9shRV3geH3EyZFGZ9J z#A0e=m4P6{T!P-SVv@$$k&G57hPP!{kIdpUg=#J&j8S9v94qiJ>SmV1G zM99T+S5f{VV@O4xehN|9q7JCEnLyqDQ;5X2{LxWV&hzWISig0+H<&H3#K(yBBa+;|Yh4Vo zL!BA;M`CiX6l`=%#HEJmNjND3ByqDOva50iqiit5vqOZlMHohK_c^}*jB1+_)-y7! z+f8-e`lJ)`OhrSufda7&sS+kbLB)|0yL{%}eub3YBd{V)84b`j?q{ohA~hgNg-{?> zLcnN`UYh7A3iLX-KYaXte$botbEP*CVe5iV0=3xg=USuMbGxB#<}D9) zt_rRkANIA6vReQ|Ywc5DE50}UGaJI3Ls#GoT~ROcw+QFAbJ9qE6I%clH0owun^oQu(48i05A%)i4n|tes z2;5yuJ2u0KOVP2(<^I@Uxj!D=2=Yp2PPV=6j4VNrLFPo0axBM5j0A6d{|&5)7)Oe= zxu_jmOxqR}_cEx*Ykg$Uu}^9ZL$F>9W{3|mCY~~l_js4~{&c zdX4aI(gK#>NV8iLImjLBkq%BN7dG}X?LoGERMmFydi&J0i)bZOq^MHAG&gb%AO%iA z6!j;exRksM>jB2i1Y!6}5Js#FZNw@7?=14jfpA|+M>}8vQ2O`vk7E2r*YG0H^>kV?ko{x~Fca`iH{Uw+&z)huBNh3lx5C((u^!QHO6?yosBHooFF|D&d zw$7I9|6X7jHvinoFV_J6rHual9}C+Z?Xm7iqmu4Kn~d!FKX!lodwHbeI@k@YW_H8! zEGFyvoM5XH4zUAp(j!dF_=_cPL2g@K5LJS1?psQp?!z-*hg>8a`1M@i3ur?dd20^2 z$O{oERj{gYZ=^rzXoqD>iE28=7aK&J)%rAwri`j3E==P%keWm@1VB!Wi?d zRAHtjj&JMZ{~~&K{3cxp3y_F(BAe&OXSg3wlzVQYykao{<=_(ywhzJhEQ(+sv%j(b zucS^Il>)^%q&M^#_9Rd`M-sPuV<_f=QmEv92@C0G^2lTaOsz0sGeB=+#9^GG49#AK zRzAtGP^)PuM6y0E!lhF|N&?G~Ve5iVjmq(>cl)ZMV9r*~&dtRZP8E2>&p}VoQJ*0v z&WFzX_jCI^E&(8z7RMYhd0FJwW{rrDb0#lGa9}Hg1#YY7q=f21p%lV%grGJgByKxa zD`9>R;ez`J%<23DT*V-9$P zZoPoJ`OGl{I*kxM;4xSQCtMX~H-KoWj^IcXfuRZs;a#~v(4v$uJ<8ywEsS7X7%5C> zPG@mS2n~QT5Fz>qzHkdWC4*5m>x`HEK2cg}dkfxeBUAnqIM6Nt8tQ9N1D zgx53}cZ$Zqm(_okum^V;#4z;Yv(Guvb9ap5dKXs~B+9#nq{I={1!M-+FP-tQ)~Db9 zS9yzn8;P(U(9=|Bd4vo@*UL};Pee@ezJZ0Z9t%_-#py||M5xDBe@>$c7yDiL(EvI? z#lIu;EsVS!=^NjwN#G5u0xnlj4s}D^2-B;5Ghkl#Y3he2b^0!~gi2L4LRl>*j0Q-F z1va}e?mPjPbo4whsG%r%i7!|<0Q$k;|`Xn+K)|0KE2$wFB+iorbzEJugALI7LmHnE4E>4br>vV) z6s=T$*kRHz##Mb=+CtR4pN!H*m^@WOR3K5>q?R(YF0^moqAelcK{_+W^0!oy+(3fv zq?4_Ad5YTNgGIobWfIkX-oXvxJ^W+kD%Xyr3~uL2n@M~Wl)?_=IS9uaHJ$1`Ua{R1 zLenX#L$k(62g%3q@yd?_+Ughk1=nyh8`ZtgYvD@^_?FuNxbToXalthyqW8BZe-jbg zp?+FfOxoveJhgRen&|2a<4SZC%+{%=)Z^oIViw(=EkW{qOkyG`gXwf=qkAG$mE5s( zhL4}-Wc5bl@w3|V!)GZ~JuGbctad0NLPuMeWq1ruxjHhYKEp=7>v z`;RD0VaAxgr}iOVPASQMRCNB2a`S^iQ>e>0y*zvr@{gg|rIa?0vG`FGs`sD!4%V5R zc!&?sRfusV#YX5lxrX~Ay1U?*bS$QNK_26jAsckn3)I*}{J(tJJ_eoTi8 z(+|2%mr8wi?xvjV#6-Rj5(So|qy>Nm+?~fT+U&b}f~N11w%VOV@s}%>VqH;?&xhAG zme-8>De!e}XGqxLIE@#8w9~k1M}+iQ)Pkcpbi;VWe5sX)?gtt~H&nes2Sx#X$HPIH zm*~W^s)#hdc@^ny zW!!il2N+rwDwazKm9(8s4{3g*16{zhhM0Wps*>@hn}t}0Zp|KfdP^UE<0X0BSL#HL zU@XXU9g4DS`0=a2-kXYSN7zRab1v|v$tSfyfym!E_dm4k#N#lU6Oe!L1+#yDbfo$Rj{}fz>{p3 z&C_>#Oy3RDSBTUHB`eF`tt@*m=uL-SUDHG;%=~ccQ(AUykl3X+^8>8D78Ipqgq94e zQm#p!3D#Z`72#2>^~rg#mM2V7azJK=CnXn7K(&%_OU>;OYFBw zi=l1W@?)uRZw}JkvJiQ97b6Y332X@OYD0Lp9b_bG3wdwUH1-Z75R$dj-+5b+bw!5! zbMARo^sWol5`{UwlK^}sIYv^Yg|DS4-+Kegjs&WTq?AWV5JYJHc1sBryTuESNwz|9xlYbe_^=qrkeuJ=>z*aax_hIF%xx0Xp^B$>A@0$rT7Xn*}BUjEJJ3Eft;3B5Nb}4Qk0YdqR}t|2+-MFKsBoMW#;V z^3sLvCIPQ@1a#Gcc@OM`mrf!}gn&sOBRr-=cq`2S8zv^w5aZIlFIeaaS0I-k=e!6R z$3_C75xlt?XSx>)Qr6J^BsRq}^O`u4|_Z6tSfiKrmg9dw+_1 z>oOvzsS|S zqYEiaDN(yZSHcAFXv!{y8Hz%%Kv4z7E3X|>kP;I#%4zE)?FEMpO#HdS) z@EJkE+n9u9B4+Y>?r?CwZVFPJQ37(R5PfrT{g;XOpaRnO7QBfsU$TX2nSSr+px3`+07&-Kt6ZQ7`uaBxK5+0WG+iSGHUY|7zJM;6uy(0s?ByX z8g1T*SpJrT{kupI>J3NG1rD7}febim8e*s^_Ttu_8>HR?mi#oIzy7wjIWFByE3ge_i#E1!qPj`6jd`*u!g2?RD z=|pq5DXfgdje?H=za<89%dONyINP}jspa^zBC8{=p9N{V(zQbihS7vg8&y%iza8pG z)Y;KhdC|Ig?UNw)@++4o41Mx_6a?zFuuwX$5u0U)?$a{#92r4dU=YAN>S1X;B&~*{ z)ll?d+-W0Bij#hlymhEn5QX`c3*2zrlC8LI6}_6TCQV~`p3Ff1rjeS9&XB5)G4Q3z zEj200&R%M(QjZwGG)ENDj6pZuhI=P!B!qvRmK+d<8So|BH2%pE1ZD+6oRi(-I zx)0Sx_d$JyJ+Tz?amo3@aQ^OFY6jYlhg}|T&x0n2u`Z4>fbB4hVf%|<+x?;_-3%^< z1nYVVfu&2YZJVXWvEoN~q=QJ1p%c73T?FN{5iZh!1`l<{R|ASmcb-9!y{!9szf{F@ z@b&G6HLrK^q#kS|*BBreVyXmvNbti@|o-(ycT(BXgm{nDwvmr9@e$ zB?_1in+oTSH`|HWhQtfUD)XwC$d`~h6E}Im!VARS^RnksS3X8L=!E#H$s1FPH(q-T zgH)2}Y2tpQU;Vc_!iaJe+~Lqu@$sRtD$ZwYj1bYl#3NuVv$)w<~w z4&Gg`DW&6-uFShTP9UK7%;a9f=PZY+5sjhk6Awr9#|-<2F*h=GrSeJbhGC9ewp&*MQi))G;@m)YyYRkz zv;TYoLP{5O3Ie+eJX=s*F_=47_rOG&5UK1b14U%Kq$t*VFbZ`|g>k%M?V=-S7Jy5h zRa&Md=>`jqtQVnX@i0XKdzUK0C{bJW2i~_%c_yV0=lq32zy}h6QLtU419#l==LLO7 z4%Yq9uWY*;)FC@hcdTDSBa?aFo4czT#8=I5L;K8OucuVO?3d)3rVhgi+^~-$*2V&t z$dPXc)0$ZxJu=^D0~@C~n?ry5El|1h_|ZI5@Ar+pV5#a;+JQ!XbY=gWsy8aJNnCC~ zu<0lRu-0umX*5RR(~qFWemR3#;v6Q4YaqM?IAtevdi6RrXdJC9d>+9cMuE?HA?MaHsVx^YG>-xvdWlpVdkJi`VMM_y zEkVf(Jj%xKuY(_=LnU-{AGlAyCx2(;zPE2#Fv|~;g^>k!EnDJlNQIuQ%taC|S_+o; zK<-+zh+dJd;<`vm*0u6u#28R7)^TbD^f!6oh5s^^_gu#3E~XY0xBT32oZ6MisoD3A z%bdz>h!+sMkf~}LkXKKzCV3?{c%2Dh*<7IYW>?z88>6i&Ib`Hs<{DMB0FDV-8C=`n zq`NGi(6)i210&{k^p&55bMSjwx|}FkLVQ< z4P38ijdAU8S(y-d1q2IXp;G<}!9f(mf*I?$#6Bei$1MS^5b8}9-5mww4#}thaUhWv>o3UOo zf${uWCn7O9B|KN26vnKG;G8bdOew4)Fl#_LnBBcd$kkZDW3;6&PF)aVEI2ckf{&=4 z1#I}Sbu0D%GUdt`fIOdNJRkum)D-BN3UtW%#~b|dh=GUj;x1VMN?ob(H%wg zxMpX7S2N1GR#O4jY`6zT2IZ))gd*51=Q2@&PAp(qp3nNC;^Zk=#H4YoCtYkU(l~9;a3jGjE>@|_jFlMyk=BF}=_1c{HI5H_VNDaIDSN2K}&ebUr4w6SHTJ);{= zm`u$xE2DPXb6!#*!UqrqwBc)+QLod}1O^BtO)xOtM>=p~#GATmy`L$3+Zgu8jM6N& zGm(BmrTx50Re7(h)+R<{cf{Fs=P)*YQY_=OkN%4moBDZ`#c%?{(!!S<+NBJFVn{U6 z*$xw2XFHCZ&IeYu&6DP<>>mpMA`>yF@^O9#dK#$MZ~^sO(XJ zc)Kw5Gw~6Gs+lSvy}wm14Mmt+X;hP@)se2EVr&&1q1}!xaX$)R!ZKhizP@(B09-E2 zMu&E%T+ulm9=|qqU0@o@&=J>6o`i^2U`iW>C*LMa(ge}pPT-%z@K)6#WzX>PEw$i6 zVDfD{2D<0CY#%n8D&Wsdx16@|4#8C-zR2IW>!q14WJg>lSMbo9y%cq5o2z3Kh~BY< zcqzk7-lxxvZB<+gfJWejjy@ej#0Y?EhQIQszNl*kzwG^~z`Ux(+_x&3U-Fuc5%^eZEV6l4XhG;+v2y0utDhDzCAi=xpj7B5truL=Foxw{tc;S4(c#f(5 z6NCW|5haKFWBa;6EBx~KIy=mc!Gzrxp~w3R8S$Cg9Es_%MEcDq`P;AqDZJ&;8-Z{T z6*|r^ZIuz~2;Q2N$#6l~9b1>G2Rl8xWFiMNZ_p+)O!;vd##9|n$x?vu&@Blj0Ham8 zD7Cw8_Hh+PP~}C#NEk;Lg^^7e0A^-{wrdpS)2(?+M)pEM6(nepy;XMGMnTPcZTq6I zOyjjG55Z%imG1q53Duo=vZN7hlyK3nX^GAT8aA4o$med8TK+R{k@C-AD0;?uJZ@0$ z`{q0+R2Ra&NligrnP?Rd!HmVCZ~Kg@bm+4MsSubXeNr9^!v2>ac0ql29l3YVHo0K@ zw2oSyL(VZHF;u3&TMtt>YBNF`_1asMFfs0#*bOJRhautv^tyr`TA0FlkyUwAWu!pt zuPVjjQK#sux|vndUIw+lx36b7;kOD?6?JDZ<&=Z2MyVLYDwXQzeZ9sIhOAH?9A!BP z-~Hewi>Rr4PGe5xpawG@<5Klq%E4D%+T(V>8pA|ZeuzphZN>0mYO*GX>%f8@Ln0sX zc{Fk~Q>m?_o=9h#7Wq!`P*2JdE{N zaKIs*Q<$~G_!hNgVD$pqBxO&cZv&KaB4+mGEnuL8|Blhlf{%5QS?;QyKoH*S<|H8{?FJ%3xYDg6@}`2Qlhfq+JMBi<_E6rko?i}^}uc_o!zN>({98N#B2I28X2@avs?BX ziqx79?GPU^7lrLoB~7+2Q>t$Vsf_d3H!kpGvWS1%5 z#}Tr!=Lh$aaB0conZ#F*?D)ccd#<$LLM>R>mZaw`Nzaq`Eo}pX660!>ZK<_XG3H^7 zX`Aa0TW~sY*Vo(t5o0{HB(_^-@(=#6-I2}-(CxE&KW3)D4COd&NDtZ$p zVp&FUB*6g(0Ef2rawJrGrK!z_*QoPP1iMQ$c>OyMNRKjJkR4^MO$ zuIe--LUCc!k_kP7PY3k9DM;5M7{I64zMIpVLi;e=&^wjnN9BVd1Te1CENw4~KmE7H zfPglCEUzZ%H|)?|768ZIP3${8xf3-iRBR$-tQo%5brEkuZ|i=8FY7Q~MbHx+rVd31(>2Z#$P2P{EG~w0ep^D7w+vvVcYIVz|(y8xiv!?AFgg=%l z!F9ZyP9Cm&A9m@bz!XK6zn-r}2%r3irp@N=eE~quSuRZ`xDbAxr|~xyhK3yxyS%FO zRn}#v&$(Uf_uTb-h6Xlu#?y!q`In~S=U&DsI#!-=idw>Tzk}sg0@3Jp&#U~lOKMRF zmpfHVrLLq=_pRZcL&~>A;;gFV8QnQecKm4(M7@m0BVu%xj#^`Uw%zG;5BME6VDiIm zXd$grIJYypM3~O6`Hdma56-eO4kD2cvzp*E zE0#?;I2TRD(vw6@yCQ~QaR8l$!05C#{34oVsSThetrX5@XPQ_$=hm({3XAD_l)J>#IRIfYxV6I*{l6kj-ScL@*2c4ZVg0lRM{iT#iR(TR zv?%mlJoNPCS%|`75uY--`)fMNlkAZ?n7XCTT?Ot)Nl<>b?5+*?%osEMJrk zQmIR$=PsBwcM+4JiI#7Vtr|gGVB>fJ*?x$KT!Jpi8m+)Re3&qLxPMO;N7JP{u{Jx( z>&bBt7AV0HBf<4>AlQhCa9PzM4vO{AJOq;i@OG5sKPx$du7>>)?x&pwaR2T#x3c0u zeKR{j)4EN2G{b#5R&lptnuatJ3*S0EH*UIIfVrvsN8l=JjElF7&*>02!VH$1!W8H6 zIbKJWG+V6}kttCm9V1k2MsN~KmWc1sf1=xW0(Eo16vX?O9Q6^lVMh^pr+yLg63F-i z6Or);b|2%9?LNlJ?xWxtqMruk1!56r7$uycFKq>auiNTPjmm`*Kb}UKx5Ssek`thu zAKgCY2?S+wY=5Nrjz3a+$20g_W|V?at($}eKZh}X%X!|G5l11)WIHk-A0NhA?$kr6 zu?yCdC{aIIF;&oePME$~VEa2`NRvJ`wM=MOrgOJ6bL%>30n5x|Z!BH_~~mQ$CNi_Run=p%5L$`V%}jts>wYETP=? z-)IYpZ!$2PZ%NjT;=ml^gx#U|yQZH#v%9xEnT0cBmB2TD-y_E=)gdblsoQ{svV0OE z?#huZIkIW8+MZLOy!Pq$f8mL=>j+6SrJ0o&C(N(_H^az!{=}m>3tUuvo4c1rw(A!? zJ`M6`ALY+yw$l7xmo2vo_$I&XnqNKhGqjeX$TW8nPefFszs{aYpO{d!*&LJ>A!g@N zGdk}w&w7+mbb;DiXE)&!J60E8%~KvE&OTXLB5kYNBz7RdBDWCWR7sgP5H->TfX+rR zGFU7Omdd1MN!BccRu!TxpskMYb~n^iLbAPt#VBTkwq#z+jG2&63vMV81P{OS-&X$e zrk3*iCO`VNG6)D)2n6Y*q1SfMi(_RSw;>;0wKCMMo9Ql5=Ti8g>2?ZF^JG@-(3aRT zNHQvSOPji;CGC+baE1qMbKbN-^S-D&>osu49LzV%6pUra|#R;HlyXmBtal<)Ml3 zdzDAZMlfNwKohOriBK4qtzuA+NC9u=;Yts%s4NTbjgd z7PHohDiO7EP8=Vr+Z}Kol{|f0XSr4#={n;X%hUP;rE+~#D%Yg-Q7}b!g%m`6=4bxQ zpSk?r$SeluIhBa)Q391^qbhX`=*VsIPBFpYyRKZhZf!FT(5f90TfS2o^=+=BAq?Ib zp5+QKWn^1mA%Xy=@b;0HGYLnMY#*)pUo5i3V^6Ar>JPh{rP4?O=A#&b6~x{q1$=L> zeH?195QdaNM3nc>!)tm-aBQUkJoJ(@M&SHhlQ-36>kCaQqk#2u;D0h2lvF}1?8{hwi$N5 z6_&QRiTNps$${XkLlWK7=u;qb!`^1S0TApnS_-`dXZ&#g!yP|C=y|$R8UN6k>^%8* ziHGnItGT(^C*AS~eZWV2fRf0_OCqC((JQl6#F!aDj5yX->dbi76|`FFf`R({&VLT_ zw~-&^TK(ZM!K}O7XWQaF%l`J+Hn-2ZvwfCr?IlqTP4oZ+aL?9LVmJ*CuXMyEw`MH! zDHA3c_wL-gb8Sd(epw>RqQ+0;3y59Fo|6Ql{UCU^fzQlNe3aEuz}$wmJE!@eIeDL( zlk>Ux(f8ck!rCnMLNf*_4`0y#EkP^i?Z^+0B6GT9nS)iYxh`3`FvXbTDIe%iu8~#6 zOsa5f|pwH?w%*KyEBX>n8)igt?Zky!}D^oRn++|tajv! znZVEv;_Jk2KYO04%o=t9yaHH$r)SI-!mz3%n7RfBu%nxaFwkL$?MHIbQqU}^iirwo zdLD;3OpPE`qwY)!vrm7_IU3M)&fU5|_%|Kn!VaM#N*#G?28;kf2D>55Cc@l6ltE!0 zXDJiqf$-g%B0^9i7)JKJ$>u?+%L+7hd0V046cEjTgS?se_0av)E2x`K8`Fm9Gb;1B z>20!(Nz{Ao|B*IIk}gZ)h~12`0DP6=Gtt52`KKaq;Q^{VP0fD zuScIFAb_f%U=4At_L>U=RjgnyBy2@yc$1^6Ql+bZ|ph#BRax>Tdb(vW3MwmXH&C~Ae*32_E z-E_jzU%&h(i@kUbC-Ga<)A}_zmtzrU2K{hO?;bxkZ9$8~vy>u8&05Ihs zqss5XdI_mNa2j8%hG!Ye=+lF{I#i z!bDl8N-IZ7U6OpxEbM})Gf2VWPBN|eU^St z7^_SrsTy7`7b%xTWuVLuGmIcE?Spx>UdosaxkSprrO5zcB2~sxe&bBenJvbm#2Q^Ay;(ZUKFF zhM6VCfbLtq3jXd33Nu3rohL>1Lu@`|5MxC_U-ZIfOrh=A(e4Mdks!EE7z7-Rl4~T+ z`Oh?`+_)gxC5zHrkl4C#F==#Z72z8P{3(AAda5$k^PQby(hqyZB*A$0i&-DZ&W!FC z5t#-S=`)0a<713UK@b5)$~a}G$^_wsX+Mvl)a_?GE{w&lIj7*E*WU9GpJ5!j#Edlh zCJ&L9=UZ3r^9wRcv{9j=C<7JDm>BRe6Foh{R7IIdg{ha^mmYM#^W2JP?7MpZg-AX4 z111WrKMOoIs4?y1U4DF{0S6xoS^{!vFoSOeqo9XnrfPv{&Q+yyJ8lFz9aj#N!M$kr z2hg#0(f$7nNQ|CLh8Gfn9X=DNTGIE^Q5}P>TWApv$D2j2_s<9g5n9?M4fOX#{MX3P zb?4P9)kpTcl*KqjaM71a-CS1ahIT%4!0SLkZk<4=nxz-U7l>%w%YVf)WRS zPk!nU%l`3?DBIUFlM+A~FN#Z1OJfFAP;A{NtY`O#xZ$$Q(tJDqS~?y!fLYPs&GqZ zyHU%)6sHIagyiVvv;7XDaC|9)9Q-g|#`=j)?u}-0jFH;{K4X{B z=iQh3!}XgXWE#r@L^zM8;w6RS<7j^K zL1S=;{rMnn4o5Ly^x#{}S8$GYG%r`Bv8S9iq?R+UQn1e2+XhI4IG}ywx1(~Uv2SF>WnWT41qw=&@Sm8qFYR3b^f3* zDeR>Q3wy$m7Bu7r?N^uyUuKHaG}d|(ng5@q4WG1pgI9I}t^LLblc4)46OhuH2uSP# z(MHaQXr}CrsPRHnLl>l7Z6*>%x&&cR%kb8d&=NL)I$A$@`}E}3A&617vQ^Q%#h0T zR+#AUrtK@yn*;NLbF}YhSDY3{!H>6l}y5} z;Z9aLXsZU*Kb|dJCO99IgVBd&Z z1>hx6`=uzRhC``Xx?QS6AQ+oYzSVO}3mox{-3cD`y%_;v#!zB|W^Qx$Lq)8+0b=9? zWfmb>wjqbDu`Sx=@5$yDe7=y!`#h>t#_LDHr8OG#oH|1^m8>0(QtbYq$QtS(Gkz>_ zo3QrbmYU>~CljIQ6B+%~>s{U8f>kU+oJ%^(wLwlQA~C`ky71_-UUj_1m5b0qLkX3+S0N!U7z*PG zoxrMy2x++=d*i8C&|*ukqZ^iE@qpG&T+fpp^P^>u7*D777nQx z9Bks1WS=b+xWhaV<1JyBM7zag0D*h;=)HRM4xL~@^*QTINlCe>oiTTNH#acW*Zic( zcl+eUgz{r$polB~e3))g#E@_ePhgmI2aZH0D(0UG#2n!9#;w{&bAwGZPYus#e-YZZ zDcpQ+jBntHuv_=O*t*&cBk8lYw-nbpqOx{%lT`$F=XI;xs@}k%>aB96dP`1JZ!z^r zv)4lfJ-G>_GznKG6df?UyLIEJ?-P{zoSUw>$uD6xh`DtuldH(0uHO7)m|Gtv|4)Lj zgb~K_*Eb2w09yX52`_a7{hbgyy>lDG1Y~>C-Ps%8erx*PD>NnQ zehj-{LSVaNfNyPMB57~UrHo23CqjfLAA#7rv3F=lu|ws}GSrbBj#u5`Xt_O~E4SB$ zeD6{=?$$Bv!+1_d%n{MqDXj*VFgo91a&BMafL)T`^>^c6KKze^3O|NL|L`x?ii78#7x3*s-LUyC3u4}{#_lPsUgqBoX~ERmH6h&!oiKczPp z1f=CWyx#pi?5N`p;;Dm>7=srnymZhpb>-X2=N`HX{~=YLf51XaunkuUwff5n1_>+Q zg^AA4lKh`bV*c}={`rr;FF$`N*M2U)MRWP*|NY(d_`9d_|G)gcNJX{ymwzq)N`Cyy zU(3Hz$V=W zuY8Up#M{Gx%(dm0#@_9rt#kxqtP?BP7Uf9PR#HT0ABC{IZ4a-{eLEcewXYt;tB3Gf z1@Ia+vu$jSqAvvdhAZ!odXjxoZ5V=(dN*7fZ&up_JTXq}%kcjZEex5bvat5LT2n+e z>1zGJm2&gmgo;uSH`8>>o-I$6u+mW==}WW+D~3t!kq+RZzc8_&P?mB4STZ={b8AZ3 zqOn5#1OYgcVpQ(Ojg{tBkgv)x^m922@@gs|s z!29oJWXI;L~6K_V<~SrJWkgH-_+O zj3e$0ae}{4^2#ONo=+_-#F`hegUJD{d6Al#{H~79hVZy{a1gh`DqT8qA2cosO2SDo&4O zsVYe0rI|-OI;Rb9{Y9K#+}eoHdA!E8r6sijUcKl^%`^kk9w)P~B~j-xS=3>Ak+&FA z5rW*JB{^IzX^$It2`=bbdd`%R!dP5jY}%4#mo-prAD*`bK-1S=Fe6$|k@m`e=F;7g zF!7(580Lf|!j{7sSPw2T_Tj>$@@15FjvO4d=IsnY%@0A%k3r2W7Zfu;E%`ogU-@9R zDEO~koGNnt+YPL z%wVKLZt|W3Bcr2I!eV6Cqqx6~OBAO!v_om%u9}^1a~Z}$_grvf*qd+HE>DDh_2%hJ z2Zq}HJL448%710}-@d^)8U#KfYCD>JFod)q1T{*rhjeb-9~;Y7j6}v_BupwsMtPbF z;l+Xj|1nxx-L4JS0^x2!hoZt^5^rVu!>hi>FBO}Ly@Dx z2!t@2QySYE5uF|p;BJs{+H7V_ti&1PEr#Nlqj$!7KgXuQ_H^8>CKJi2e`g}f3mx@l zhwacIOfa28s@w@?wGGEPTgKEFDoo#~NsG#Z{l@q1r}&gZd^e2%GlH1WIFxo3mOn>p zkb2&z)6&6n{CDNcba+OznSycXz)WllW9<3YQcs?$f>7ycEz^DJVFvr2t#8(j3_MX<~;6NkV!tDvtMZ~)l*J!Z%KAq1(I3DWL6i8oQwR*GJ({UiAWJZVE z>z3>mPQ`7>OVV+5t-3F|_&T$5PmS+*EmUeE7 zh(LUv+o-mEs+3nYYf0^nQrL08>5lH#Co;-*c zkM+2EP*C{ZYkS_}3fXZt4;mtGx>_MJMqUVyJHzCT!zoXTd!7oqlXuznegdb(gk%EZ z9;NM07H;&H%-iHw4A@k@C>^9ycmC7JUwK#$i0WS`O zi^}`W@>Z#@6H6k#RRiI+!R`hGAU@72Nq_0ezQ%n zcGI=5r_YGIH%mpdi}qiJ(&h1dr_(+2=U%7gmQsm{l-t*dCU|m;rZVoTLSic2GoCt+ z4uR5t;?;i0ckdFvOn&H6v7+?G`97u~X4c5hRaEDPPn;jWvGf?jkxp4mkHRxcd!DP( zxRe65)I{1GPUMZ8<9t29DJz*x_m0Q(K{xV4FjY?~m!iKnmj=DT3Ac!A-W24{TUm3w z_oC;evK1mP_1w{Ws-Tz*aTpKnsaM(?ZDd)n@zp)7j#(a`zrV%ki8wP$g!t9O*2z}6Pw9nOu%|z< zFE)6Ii|LYI=-%wtFL!G1E^^$+_UQZoL3{D!R$jAE>yJ521+`L1N@Ghd7mSIjuFSskG02>26 ziTr0K#g`WgJfJPk%I-rdMElHBUtnv#}t_*C->WSdwJ8ub&b`)C3 z|Bt~$+nT+9D7gE%HK8-ET`1L%b8Pb5JG}5u0spjzf zr5)|2hJ;n@vr9(6gl|x=Q7Tx-6Qgiu5=p3<&c;NMb8eKORLb{`H-LMU@0J!;Hj8rH zO!(nzGkMmX8<^r6&i$%oeBk=Q!+~Mwq8W;@o=>O^Ji{>Lm7w8aC1}ViL8^km6SKAg zb1Q$B@Htu_$7F^~65pONtPlbnxEYnRD7gB*8t0;9TofmB82V=j7!{h~(aEQ9R)?Yl z&gn6$vfy`#LChBx)ZZHM>)zpQ-cOm%OvM;t~BA( zpd4HFkb8Irg1o}ffQ%RSRlo<4!iiyo$rqQ;AuG0_BB#~Tl7}-*t))+1XMu_`A{4xG zNgD>3$y?!#IztxAURMueAHn)&Sjbvu;VN1bc+&D=mv?aZg0_#%4+jZ@X8f!iwD7W) zokDmIytMn)x7u?quj49h1546_FT4d}{ONDf`!5}N+@#-9hrS47wP~SqTCuFgt^#fs zml4YOcudJgXY2K$Ejt1FF&)@wla&;MancBkI|RF98N`#p3Y_A;X@rT-Ay=_2LRkCq z{hl?uCe)tXH?+8Oy2V+0o{zUdhXRidPSz8WOrabr4L2pdt)7k5o*X5-?l6qD*4<8V ztCW}BvbD;!Xt~<{|KaJMDJ{ta&C=)!>%iBCb>QpcI`Fk#2d1@a2w{E*p6 z;6h9b)*i2um}-+`ZRoX~(42r^mtbg%0!fqh0O30yKJ)P_>yL!8bHxvS_}<6QovWZ` zck{-;^YjW8=_w((<{i@&Cv&dRyz zlfx@)^(lNGQ*_r}HeI(@-8mu>+{_t7%?wHW$jpd!lR34!BvT1h#VjP zi5mX(2PcIpsVc-3)^e`tbVi0NXso3(I)-K~DBZptBc$y(o_ZXOGL`O5F(n%nJIgut zhLF$ssX2SnPJ9w?nB5e7#Xty(>uZ-|PKFp9urQvg-pojN<26}A7y(aslQJFFg>=yJ z1&2-kvJLOV-Uj%mH;V9yvKTw|Di(|PsjNW)eaxa3U7?x?@Z(vdgk zhLL!4CTZ$O_bV!HBR}z)_P|!sB+%mK)Sh^$5zjMhjf(`FkVZ@Rb9tWUw^ymytoT`+94$na|YDke4*$Y2FnJ44bv-r9E)*#NWmQ zNkNh{D@a`JF>Ta}e3pqQmf*t}W)%~-$co_7^ttn)3^K*;V&5`~bs;08ekuWpTbRXu zRhMJAwt7-Bhn4I4O9u)%A&*;ktRvPPQzYn!NbhY5obs#3YAS{q@UQHI^>c(T&2VTz z&H-NN3=Fs&%_w8A=W-&y|I${^v^hE#m|RjU4Ux*AixE}VI-&9_tVLOSTkH@t8zzArih&f}>Tk6$_$W5j)b?o8{fOvJShYD#mqZ{91Zx18e zolS_G{9Dpq<2YSKFo~i>G+W(FmQ;~3`xxL1CU^v54~!uEbRt|4CzLPh_+$z*k0Sc^ z&;q&vdgya++h*wu2>E-EbST1ZQlFBVaY|@nutzwL_YE2bft|l+5l!un|Dm(!OsT2{ z?hY8pxMjkfo`hgrMhKSs80u0X$q*@6nKXpou&0z9cdps=?p5ahNzg6$D#PvToSIL; zkOb?DDM%>V1A;W!XzLwBWX++E1yPdm%{k?J$uf7KSH$+MwMes^TT7*)F7}v~edI;B zwGcjrzl>~kcIv_L0AoO$zdt_)wW*JR@}%B}MgyABX*4_0ir?r0#Bp9Qb$ChgO~h!Y z(acWZhL);Re`zoZJjpfn<9Wrx&Nvlzn3p8k+W*o9=T$9bQuD)9-7hm~&ClIa^-fkC zH_ustP=bSqxCyt0XtBE&6$k7G2!B7G;avn%;Av1n6A07y4{@4>7(=|V??1SMV12_m z@{UqH%|}dkG+mf*2ivVi@Cs|3^YZS=Icp+J+1(|;npBbuj}$z4Z&1hq|h)(VFJIcONT8KBBxwDNJE0> zSllR=Sh>37rs9Il#ARJlVP~A`Nnz@awoq2iix|^+g1{yg4j^K1d0r1Z_>8uM=Blul+Hn)(u3`V(HQ}0aKW73F0rp^iUV5os&Ctx$h~5^v%s`QSSYOdi&yaFm2nw6oqc6`S}F%c zg_+8jKBc`dV3p!b6isHT=9@W7?q!{xbEr(D4wI-l%ZRh|Thau)cc10T%6~vVUr}3G z(K8O+cv5Q%l8WBVjq~d3<|M+5&3eu);98x;YE0CQ^_-@()-z6Mt<`k(nxi&oY*Gih z^345u!!033#E6$*wiFSnitH`T=!?hpnIi)OXpJuhh@Qy>OS zgiQ=E@HoA+Q=Ysr2GWG7xB8y9hg)(dw7ql-Cf!SR46zvr2|}o@GE7>%+hR6tJMlCI zPKGHJ-Oz?sI@5;pZ`-+Op@^ny+<5Mr9Z~z1OPx}o8f@zB-o)71NVpsDC;_6v)RNqC z!$hE8&)GWgu=Hk z?cvQIU1VH2r0eJO=0(Su7GS9$E%+O5r#b^#@Yg^57u^7Q;tU7h7O;fqO5S|_!8^|NJIIQJ|FDR0oktlv z%nSc%$_sbWeoV{}RZ@2HI->LT99=}V8QggRKkQ^!28JjzpEKQtk8Kn6$@BQu9OKd4 z`_9RxWM05eT}u;h6qqc*&bk5qL8HOXGz|nFL2g$oPw`NK`xKQzF z40-q_B5Hne&;mjlPPvG#9r8hBs?~^c#sw&pH`qV3ZB|7G!WGe=}ifY z`5KOw7WRf}sYt-g$rPYLnNn4(m&%dd5>5lk`?`fGZr13>S1ZV_bCh58gi4}ECG8C7 z+8U07+ofN_B5C2>#~NvtQ=GlLRO(R~l~71iL#{DPNLNMCW-v-eTW5-kh8ECSWt%ys zTRZ+6k)Foq(w%k(l%-K|xB_4U8VYqhytZIWXax3`#+_qk#HlE((|8>F#@9#;W2KvF z7gRxd-$A(Ve5QaPo-OP|YT=ZOf;Z7_*hsoK>WD$R>bLi%wpQmJ8LnS~r(+Qw!1af~ z4#my$k#N+gAHh1Hesxhb0s%&=ZxIE93!61EgtCheqrgXqQCN`OP`m?$_Lqthuypk5MOn!K!3xSuh*OFuOp_xyI{SpP4>J-)HDkzqzv z;N0fAKW)%X`yoN}snZL1m{2PR2F>_FbplSJPu%f^!7j|*0!Jts zj(WI&2Qix%SynLPUN-!Sjf}AH`MlSC*}zH5vXzmtwuL0-wpy5IsR{4cSZ+h>6GiNE zC)O-!U1GwCw`G@=!u8aK`w9V+owxQZwoWA2*OFXyAV#^$aBKUbA^h(j{y%*27fWCg zNs}l=oMLzD-}x`8mpArwaAPkkH=fWee@~&HP|6W{GYCTC?k! z-fce7o1iw&=>O>U;KtwvZVVjC!XO_BPJh21x_6Z{Uv9(3n=i}nMSmpT(bC=4cCb!Q zWbE{W$;qsgzhn#lU{CEH?l|{$A-no0j7r$s9O4U|Oz#BNU&~w$tP1O+b!F?b)W^?5 z6he_X%AbcBl;{Q!wewMI{h&eb zoRolg<@3yq&~C{gJZ=Y2ux#II<}RNHHdf{zGE^3!y_m$>lZY#Qb!C{WgtaW6V8?w` zpk)aLNT0Crfn>M*AV8yeHX-?>Q!V5t?3ABAvnTe?FH1S6FVn4F6KaSDg?pT`|tqZ5>*DFRHp7pN>b+kKMv7a3wxP#Nog?x@2mi~c~3 z@>+8AqIldpKRAHy>tN_gFe>5^kFy|T<(VHN-z<}VJ>hm?H@F4#kS+zJj@lT{;evgB zia=Ihgn>_0&wHQ8U%lp@L^t^?+K`pO)0<(|dr0&zIXAHsR6xva6<~MAE@J{SJZooa zkUZ#~{vx{F(&sty;mH6$N+$yNYpHLizvPHPe#PN}$`_@BRO-%u8u=>^t5+rwzKWn& zklto@*-fp$R?sMa^$ZCw=15C@fkCvB>GKr zi&KUK0WI_|ZJ}i7Z(Bg&WFQSVsiU7uY6cf=1(#?pN_760e%hXP8qjZP2qms`1pQfl z&+NZLB+?!~;Xrzp-}U!Xf9;;pe(gs5t^&#X$LC^={Q_pH3?#7^Gs-5(7d<~0u&HE_ zx&t8+gj9kSaKYwoF%oi+4z>H-x$^Eb8(#iuSt_j>C*sb>-n^7+FKzeo682OtMXP$D zL(^WmT@H7DDPyG9mZqZr^)FwF8u71xEqAQ+uhiYs@1Fjx{QpnCdpi9~QK0_q59QyV z%D?~ah%U;^zx}0L`k$gl|NHN*ug!Ds6H)@oi(Z=}Rg~AJU2H-BNx>JArV`12|NUS7 zK(~;^oDCfnMOlAs`>R{pFc>dQj4EH->`G9u)i*&j!eS}S^9qBt#~3TouSE}g?fQ-m z-r(QP1dk5^zf68_MerLzO3%!eBm%y$6mPIQM!5wZrKzBiPrf8DkJnd2Y7-@?C@2cl zYu}zOBb~PO${siA?=@z@1?J=ts=6a3mrscp_Nt?)`kai!nqXJntB{iWsl zxh-wmYw5QC{qMiKmKsD_IUUbpb6rJ&TONNQ z=0ztL3qG}Onb$S^!l9uF_f|w1(-eu?z4CLCO_*HJwYlh22T_rBVFL1H8$p(df>4E2 z<{Yo&CRT)t#LOy_iPTq;+{w=<0avq6$}I3AoN~%guj0L;>=kKz20yTl)6FCK8kD3P z$szmP(U#x^o?&)-V7ZnB3_~%7zBP=H+jKC11TC%6X=wKi0~U`l=bG7&d5q*^1n0dd zW|phta{E5HB|Z_{S&<<9Q`e+qnU1WC$&p6_MIS5j$zL*^(lf#9Q~^scPrw*f(-iaU zPj*gMP;_1eeAr-00Thjys@q?G&!THyMaE+n1OB4dNS*l$s|W&fM-W`ZL6}2ir4oC6 zi|u1j%$lOgn-yR}YtfKqdu&UsV{q0y65|7M0UUk0V6|?xS~r80mZH+y_ca+~NWoPM zlIfOUn?&N5b=p{!o9=${0YeS(@tt)wpQ~z4_yD_{oyoVP|#$B|I|&ck`gJum|)hD&5Jj(vH0Jy`Iz+t zSsraT!k|ueJxojPyoGQYU8J#yh;?c&b!smuwbSWbdRdzoG@{=Qb`eu$!Fg}N+r_Rx zVlF-yBOoJr-enoNwnW;kSd9zv<*)+RWw6-oU`nAdfB6osl?_jj5W72XHuMcYZjU)3 zhsy{ue4w`*Uk@?4QV^HaKhZNJrV7hM)YqjurNxdXr=(aYfCv23cN#5p@8&e(sfbpZ z*uf>2qIWblVVLEZab{eVL83H+-WRqKr(ahf3D~qjA zKbFDB$kIT1yZ#cM$aw3g|Gvn5lb(ufyoS0gb59>@P8Okrswur=9$w9oC$?rvl(S=(VlHJCA zA_?-2ukr2fqY3NPZ$DYYk0cUbQ7^LV4D9MKqVX;|p{ov508$5mJ6w5>t)$u%nU zI$^Ao3K~#_7wVg+4+^3hCStcc(q-3V9B>_H^lrkZ31_Fy&JEV~NEo*&nV^;_s#5_^ zX*VMGFhP1pVpM)r`t+sIUyXG648AQ)vjlmuW8|$2qdLpL{EfmAt(zrNt5~caOJ}W9 z_I+V`JU7Q*S@9(@B^2t!t!vzyB*yB;s0v8iS%lISpvW;*OfPZEk*fodqYCqBr%@^t zXmX*36$me4atZ+h8>6c9F%n9FQnoq8nNVYd8L#7Ud>(iL->92tFpiddRhQ8nM~iWg zr92x;6%3k)pfq@)f$Q*Cv5myf5eh$t8Jl@lqQ$E@X6-jG4*Tb!tK;!`sP6y#4enna z<0u?Pkr<56XFXD3&P&#g1tArKkq=|->=}j&zDXFjRtYL*9EciP6Yy2zkYDBoKZG`N za4|rmP&DOJ1#KZopf>Ml{mu{;i5bsPiby_1cr_ z#Wsjv{PGx*muYCLeA@Lmd!mHY zM3G}qCj?Zvg702}&%K6EZbEFiufV(CFwT43(-{|i9Wi1xQ{;8A616T?qSggU-bz0F zRe%|0!ZdV%v++pA9-j$NJj%#vhowIubY`OM-{mjInJ50+Ud~7+;j+mu+2Yafyq7_g ziSH(OxU@;3)7&N?Pk+ka6S~U>9l<0v0WkS3gWDz=ucGMn-IMbB)DDLtPna0-@;TUrToe8Sw_}?G(Li9POZ_? zJ%Jn(g3n?q>W1J-vvSfbp@ItCEf}9ta86Z3ftWW;c#28EW4jXh{c43%?Mf&}DB4}6 z`}Q#KWf+Q{nWM0zAwhrIUpg!I7cuzKj~XzHY)?;w=I=PX>u+nn(ovH1W`gvWb_D1jF;wZiTdW+FOrJE*=70zEYb`KJ4ci63`+c_eUQxuK;fAfao{iDIIaE~v zEvgxjDF+Ka_fUYn^Bo2v1s!*pWe}NqytvZ3BpJ)%cxjBkqq`^_g^xsCmnrdm_lKQWZ?bc*q}hXn(p;x*SvYtL0Rus{Dz5ihs? z;a~rF?3~aAR|eQ=3f@zwrg5nV1081Eg3d6cCKHi`1Vj;73Rv>I16+EOcp&VTE z=3*sqRZkckUtY87d7XAx7$dml6v4feLO`o>1&ebr5?+yW4zrC~aT#Tp@8dv9Uwon*|*=@>CMj=cp&=Ne+eWdYG zp*GY5CLTQFjvC^w4W>{_UPay>39LR-_k(wJ^~<`4pt?FNstE4tl0d(9Ct5xs`67Qi z{b9vy!sCkshSo3*vFO;^aGakWI+!%(fwZPQ&w+~<$x|218(xOO#6$4ZG|(u#o<6R1pRLqfy^7Gaynn^KkyUCs_^VJDbKnsC;A8c%t4Vj5${rpPHfih2ur3PF8j zfW~Y5R(Qc%2Dpx##6=4_clx6` z++-_7EMbRVupiUS2&V!kSyrN7(d0ted1O;mxNsC4lrf3>;2Q6a+EFacjE zxE{?p4Yk22_zt0Lm{;I95BX(Tf zo8koS?L1&;<8fNg_`vD&+2dInocw9ynfzkh(;Ij-I^eaN6I`%(-`L}<&N3>(^(LWq zXlZZglh$x`gDssuF^svl0&+1J7a5=0)T}bmpee=(Gm#FPHQGvz$e?*Mg@tHbP=zjd zoue>}YijXm^T}Hq5h@6ybK6sP&wG<=qA-|Xbga>Tx`=wfjiweG7iIc{MVU5X@f-Gg zg?KO?No;;Go#?#|-CHVgPCtavcc4l;k+o^3s~|7U+XFW~^U_Am&f{}#1Bt!%Y!M5Y z=45^A)a(y|mR@`xAf1Ki9J`tA_s(65=IqMeB*qVS-=@#!hnx5N_%YTjLhT>+GJM4OFeY>gjO8BI6_~)mX2Ct} zm3#w)Y0Ly>eqru5%pe(Kba)*iQZm%k{kKNlL44j4xFqxyP>YvZxWeNd{VJP`3T#tR zimJjST>jMI!B`_$Pz#AO)oTZ<5XREDlyL)SAiMXb`w2>aERLa5G`@6`?e<-EcGjV{ zGXYtBECj}LOL`TUh%)erlyWcEz!^HGIV)gulJCA4FsC`z87C=cRW$#kATP|$U3zYe z7rRFXx{H(;6W&db!aAq1<7`ukb3|a7a20J{#Qg@}MDhUkg~nt%OlEO9NHC~cK|oZN zDX+lKdx{6o+}Ug;BlH6%)VoU~EE*_|v?4sGd=eQSb0Ijp6=(N`k`M$_OpYK&8$ znK%bchH#tKkl9c1Qow7%CPz(TsI{!f-*1B#Y=)YVJB;mT%(;DhYxmfY9}Hv^X!s=<*sX3Gl)@xLRrBj1PuvuIv^D7`hBO$LHDm=c!65)dSw|L`4Hrqaj3E&+fFPv( z*S!Av^>Ov}YrWd~wOVWaTCKEx9c(=NHQV)k{ib85MH}8IY~+#0s^#)vq_MG6@ zb1+c_XqG+&e8o#S)(=tnLF~qFQDuJQwJNdp(y>mT%20Aab7V!S%qbP4Z6R{ZaGAus zSLM}`V5P4N9Hn`g*4S(i$Z}&n>}!hoT9>Ew2j!`7-%{4E6@sF1RVMo7fPWj~atyP&X3JbQrh4?{f*&hN4_h15ZnA^Du6IRR=Rl%Xc zvTQhjBNPo8q2$%GLqESI=F(~ZP2^HxI-g*?!Sw0wEz~~Jc7Ar{c77ny^>k(cf;|4-9Nu3vs5APY0oclzm<-8I&{_hk3;?Jad^0A8b5}74-EwB=kiFF zsb6p1XG$Y2WYwdkL%vqjOH-GCmvuNNt1cz$z=*0o;B>FOQfd;0yb6b?f=PdVe9T(Xi_|CTG$&+lOLx?)o1kG0{IL{zO+y|4>Hi`8eQG_7L}j zN2^RckNx-%jj_(G^LV+A!($OXkHc}R?GE)qMNIA2zNW_CLaY-UGy6%XxqCnT^cTB9 zP&7P!v%vb8GU;0h>(np9))&|X=5o|`;XlNQ>iheyPCIt4_XBRI7&;fSzU!ep^y^E- z&>`9?%=?m74}i^&JeIQyjqx9<0acnG-dq>TOE;_!Zy~NKTbFUF`=!f6VH=uT&D8Dg zcqIRR)m`IUXGT5I%iMB%c;hwZRRs2jhdcU1^({oD89yP}|82f8Sttqf`H-mfJf(13 zovPPTk?Q{UI@bAdo!$@KLuk|Ik4Wx%AK6P~dIfa#F>EhOOCS$nm!7H)nY`m+m7AJy z5NZVG1#JeX-#DEbmt;So$g4@P0MB?dVSTPsil{2TiaPg$ckbXEkeT^Gt(n_1pRZhz zbrKY&dLvC!^Prs#WfFreppiMWg?!vpwO+{TzGqc z(ziqGux92#Rb3vs;N_t*F5Bq`+L(3IIcI;^c^7Sc86R~)I-{qiK2LQX-txMDQgz8D z6;h6$@~8+^-#1o;rVQ$)wvGmkiR)C@alt`}`j(5wmBWwbAw52} zsxW9JBM&AarQoS=-hIaClY8{vNGm2&js##4EXatFQ)n&_Y{W__if=u%z)L%Iv8hvs zt5uQ1E4TGQtIU`GuS-AB^5JT&v%{qaH5jke%6NV4|M>i(N}m7EucwRu=V$y#WyQ-` zRjaQ+_Y0RQ-7H`EVf;~7LmG6pMF03(L|p=c^z{EDrBQvTE?xisz554!DqPjxD>2%O zC2p;mu_ETKJzjn=;(5H~|GfOrp}<9&?fJhRtecbS(vM4^e30q*zi9*V8~BkFlv0&3 zMxqdyTQko{=WHt654wxr>UIh^jIg=1ofV{Ozv^Pe=UI0FlfY&ZB(Tv0c`D`AArizR z^5XXz5m7h~b~5(Q3c)L~;5QdMT(mYJ3_DE-evPi_3{KZKA;#xvzjV5lPpKp9^EN!j zB7;+T4<6Fd0u-|EQt0F}4ZfHv9YQxF6gcxS)A3>q7|P0dfXLlSr-z9O1I6=bjuna+ zjwHx!tEMNP$Ma9Vn(;X=Sa7}`z#xcGEZ7LlY4Wx5w4g+!<<~=6u&L**zW%1`W#;Ep zq6=nJpYBNEGrWuxQKtR*hVXCm8GC^~;|!=G4}SQGfK`QAHwCK3(Sl2G^IsA#>KBkj=~uZXn#c>%4r zbQT6%-DS{4mCxid>@z3OB!4~7DQm`rp|~WEg9WDdO|iwj0Bt3fwA19;z5MR!kN--) zFYSbGj0n>Qp6Fi_p3Mb-mRxX8o>WeESdBZ;LH2N(xHGoiXz)wO5+(r&}s%r(1d? zoOu87-{~JMyuX@}<7+lXkOioEhq5BmZLLChcv-x{@xEP|*=MbPvkC*KTV`kEju>cY`6!o_Ja z&KRbz8gQ|)Ks7;mv*IS{0@kc=;⪚-hS`~T|3EI4&EBw4fbaNqh7ace9O*GpM<~^ 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

G;!9$)6zra~ErR$5VVH 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

3=W=CV)RkP8eHrieN5%=`Rq=F>^ z{lH{moRP$hG?WrE@bF|H@|l!J-Vf4f3J+Q$3+GqF?!8v6mpMHT16oCe$w`N#YQ`8< zA7QRw7fHxrjSGazf+_Anvgx#;xkK=f8 z$b(aU!n&^}My5SHQfceYxc9`S9N|y3^OSH0*4h%RVAtu5ned+Ke@1)rLTq4E*{MT) zKXyH`Aw`dk9hg6}IoF9V+D=bH-1xPcoBGU14f-mya}$-j5CTUhijhykbYs?;Gkz8C zfd&eKd_vF6sOXsay(!(TEb5Gxq3%?c?m)qRVsmf->l_K3&V;~0c%#Xs?fhQ6S(-@s zED(~~fXiMPWl~11*m|@|qiK!|u6J`Ag$O%>1un;ek-h`ToXHoa?m8&!efIkN^PevF z40bW)HWI;8P_69J$yES-qd=umJWk8F)2>qwm0<2gBjL3D0_X>rCrF0&1EZQIHVjm| zCBpR;f)50>0`Z73!J9(ef>62K6u_MEu+s>#sZ_?#^fO1w?h7*|6{o)Ev_uvAWNI5Q zo;C$Op}6m=Gi7Y7(efdt3XI@C3}O*|0zw<$@q6{T`nm>-8h8gr^a&cp{ei}l1}eR3 zFl213#WKl|rVazoghr);oe)lG$Gs3=VoD0t3NSGPzAreClKh$VZR^3eO}`H^K8^S* z5#sq1qdqAe3KNT3R5Of`iGun&SqY7)GMdk(az#4fa&#)zqRdPtU}8`ZH&P}b$+xMH zx0vAQVk$f73H#`&=#D4V`A+B5S+XhhjRACElVolg#4d0G<(HFIeL2}jiza!qXu>Cp zCiP;`q#i7q@V%l5pDUV_4oyP(sBY3?P*ZI*d{0dw*TYoKwnkT>1_W;y7{8=q6I#%l zVp1|Sar=M{eD8_6g8v_%D(9MyXEvmE9nE6VZD17onVi>lCKM^jM8>o)!8?9$fy(C> zlRCbenp|xftTqi+n+B^*6RJ)0c7`eK)D*iPOi3obu2>YW>m%(GF$iheGhymo^Lk2L z*<2}*qaI+VoYo3-h@3);fFIeIl~)B2$LIh$`9jrHkJG3h%pYK`(G1?@5kCU(JdO9k zyrCIVo(KB`QZ6|4mkDBm?y1|*+Y;BBxL5uM1}ko z4_G0E0+mLo)+gL)*R@8Sf=tkV+suJQU=QIZwZULR>Z1<1SV7@zt(I}?ehUDkW#>l zJh=!AWIE6I;IfaMQEbSBKAZ>? zdWyqzSKJC?3Icc*-eDBAQaIEIH;KC9jK%V+O~1KCvGJMA-6p0?rpd}s6{10b%keHH z{U#Lko0P2IH1O?5bwnCD>j#tKOM>Z|PGE3TATy3HVR8t62OC|XnNl08fih(fIEcR+ z_sw*b%%PL1+>uYLBq-yJoTex?*a0)BKG7@yf} zc9#Q%oxCTW+4oV$Vhx!J8L;Z3tjMUIdM;0(0|&)nPN12=oK=;@!`{S8oPKD9V@!Z` z-3^7><;;S+*Pudt4OyZk0ky^_PG!Rj^uVyK&XT044Tc)e$xt)9CK-Uc@p6XW6;PBO zLjsAXSAk0N@ZXWUDWHO~N%WjbqQ^Q>uGPij3TDV60Urh6t7^I}$N+&aH*(mKCxx#b1zhPus+@PsO< zp0#ZTHSZC+(Rw#(fJzv$fruZ;yePFnk-+W2iG)vOrwTa?bhmGPMJ^WK<@(+z+I)(+ z>;EXBXl9C?gg~mVV5?=eI)j*5(Y|=PfJlhO;@56}{T6!I@Zw#tSiUv-sBl`$4(5dK zNlnXWT-%;_(fu%8Z0?)>=YRWi5?T!u_DjXnE|YrMWh#`wO`rtMn@tg~Wu;rO&JoZp zE-;v^)mFp%(xBu+O5=*G;LE65qUz2 zw5@8j27!^d5Y*cqB+Jn@W>H2Y%Br#8sd^nKFohFa(IN>EAVQMJuuSD%!>%Xkfx$Nr z1CeLO``vW!`@@aq`Yv&WmUSkj#&|UI12|?PwCnurOJxL!gjDuQAwk5&%`!hFpX5mt zQa~3qfCQM&q^G?UnGc+VP>9V33gR?0vuP5$?@-Fb1QI@E0usacATcc6*Tp8gZzppZ zGg9C&VHKY(?Vtqpq44EFJU0`~O~-OO62qMMm1C&1TiP=5rjKl@#gukR$b1uH+9UDi zO-yMYgUlTmbE=AN52Y{}=mJuzT(JlBwOLQ$S*^0J0fE9#YTJzkE=7P)ue;trL7YBW zKe{38H2PENKKv|{9eyh9hJ+pp81MC_Br({IKm7t29~&l&+4d<&b^q<9w{=6&}BeuTJIGeWoQEfiOM=5`8m@8JXFz zH;c@XD>Yy6+|`!zc*RtXS%5iXH<;;G@n^W__<)7Ky{h6!J*@p57 z+YNJF&E_eSxsGpL5j@C`n8!N^`KnGXbCX^)~*ubf%%J}?kMAc}5au6r7H3m+pZ zKE8^9hp8OE%M3e2s6LYj@y?bTmYUUpHm5ejRp734x6Q4ZgU_%Kom-__axQi4&ZwJk zHoBQ+7|!R%L1*(&y-h-}b>O+q=)DRQ`r8%5StP}0SJcplI?0?L%UPA)LoH~VOg`WX zXakR>c#J%7%7ux+V%{TXMoLLNis_LWeHq(L$V7q~IHLOM=RrUaD9A9W^_t5p(mBNU zo^Us8j#r;xGb~UyG^g~tLbD!Gu%aP|^Q0NCQO&5q&UY1MRT@=Q8mn(+6&on!07p(H zB(Y=FPvNSSY1LqHhA2>=?vQyvvjXZF?=jAcr3KVXkNNnY=^yI zcfH)~1z2iHYTCK@see6Aep-0O&B0|d(!)gHXR8%FU?zvWmWSCZo@5hHpA!LJ+ummd zk=fO%7F$uZ*jhbalRCx@3_7r*=c?5Ptx9RoT0IU5*f)tFTJk0!Hv2WJ8CyZk*jicj z-GNz^0|@m%Yxhdp&S&#X%bN9tNtCC7xK?&oklrUzLW!;78loRVfS=)KUDoPi*Xlk6TuCGf`3NL+QG+-%* z*X5`nN=-s5Y7$xtO+u^EB(&N*GrN!|V!1XSvWxG*Za@7{PiY2>^@E5XILI-B{)_5x z0%cuqF5uu&WJaTJD-u@mN3rC@s8N)NpZ^?`l80AISWIRzfOziFae>s$ri{L zHk_{=fZ6pSA}?@PI`>V>&($i&T}$1#E2_p_Q8Vs}igDLNJvOV+V-vAzP*G(KKeL@@ z^2lPP0op9XwG}q0BSkr@Rg|+n9EbB;pP<&8>zJ1!D;<7|sRCo_8Y1Wv z`UHeFz~lGobMrVJ4CiogQ5*Ue3vXQ_bZX~QE{dwLOJ&ex9(>a04)o}hQ9eh~_N?w3F4zWC>YpXP#}_|bs6Vx3H$(Ks@oYo0d|`LX!V58j4W&5a&(fs zTHK`DSF2-k$0cV1(7o>~`sQdjyJdhvKa8;)2&;l3LKVg8)OHwQC9BT_gdU~!$&hny z1$lh}-3CUXQ;w~)onmTV;2<2rR)Q?Cl2wdV{0n)F9riz>Ih~F^Ybyds%;k{>(kHx= z>CLTK&qXT7`dJt9z<~l6z1fq0qN<1|>xqzTd7?gur|(B?bcxCzd!Tcb8T?*)JqFO) z2;~bZ5nkNej==uWsPH1(5n;Og>)-zN(=h$?=KuZ0|E=ghZ6Hil z+g$l%BW~k*qQk8x#uGm=Lia5qOwER1?5oJ=F3KNw*^Ez)!lKEJOJA&DJ=bHC4{ah| zx}X1xz+;A69}&tX?ZmgJ=}r1CSoZy+rF-|o@!qKqM(6HObe=lDx1=diPDcW} ze@rvzVvgk2%bcC5OMPlu6Ue3RDBY%9Bdaqhkt&!(7CC)s271R}Qh~Yk+zcgSz8u3l zOsN`PwxM;MjSR2&o4aU*Ftp8Ku}MN)OxNG)%eeJe!D@aT+2lwhji-L1t?FhyG@?`%jRF13KAqkX| zlauYN$TS1Vl&b3QRlA&hrkXr_r{Zqoy@9|iou3vTw9{bW7Yv$iE;WAcA$Dh)v|ctn zNzdHprD|>eq^(rZpp|YpU~>>qbFFO^%KWSfyV-rS9$k0PhVH)7`=nr~_r|XZu-up^ zJCu7{P22gX3yd~8F@fBBRj!FFKfkLK+@0#P@6Pt(ch`M$<7}vtR| z*4e1G{{O1Vg_%6XkJNoy?3bkX`iHF;onL#L-}SK4)t4f7J3p;aG3#odVR4Hs5u@(c zIyaUb5|cXBL^;*P_0@8>*bfb)avd;eWd~N}O`vq<_=V%^+E%L2`g$RqlYHk164^H@ zq8_T7cZq~yzIS7%V$RBi^!!ZmD!T5nR#*JojeeM#ZI{9i{OslULoc2*PB)^+6$Lu@ z0xgGR(hq;&mgdMd4m@w>xoCngR~&o=pH08uIa~V%#|nat$5Z{Fa$R>}<_gc6gsUQ~XwscV!uF@@(q5@xRfAZ!lN&c_`0I9G{ZvM$eckBYy^{MH z^Dzf=do@)TjM~1?EfJ|LgdwnJ?N>pP)TdL8>gmhCCnJZdE;qC4Enh{rh7OExOS}&~ z^o;Wa*c(aJYZZ;3{|pA-y4lcGfsmTf@TL2xp+6&t2oWTF+y>;XA>8ooP~}GI!PKN* zAJ_l*-wAv@l$);y1U^L7N;otnHjYgUmrqF{1WB^>koFOL9z}mAAvKIO6%JnwOnMr+ zpg%&^e#}u~Zi2q6Ilohhh$(FDct8asCPja(!Ax10cyG@1*EJX@0%FQe2?V!s)9;O+ zyPKLM^8GslicQUSB;7JaiC+4p`p6a}-$HuJe1FIW@|k8B%_}BU9pn;>e%wVLfd?qE z&M@qiO|lwm?x_Ry!SY8PMb8Ce!x_gLNgi-^CD8LiFPJ$KA|OZcYwskQ$U!jXCe=Xw zrcH%&G;Fe+Nh-f&IM^bQySye=W~veR5jVczABj@_7BX!1O>M81F(*6`u>)uplch_} z)5YZDk|qDioEN{%(uYRXMZkr-%+N{1J@~2FKTcuXef!J*@|dQZC?pt(oJ0D7o`~}% z+?}t9zWwDt{Qv*-w(iXFcKIR`rN@tYG^j<{!HG-5jr90s>VI8SJjOF48mfH$u_;Fb zw%=r%%*hJM&DcRYR7}r^vo9F(DZ-Rbd!xU7llzhUd_Cov^nlR&y-mKoz5V6Qv$T)< z7y6|yr`iAE|KrD1^)p1k6*$HzJTk+E*IOj=<(`Oh*aXu%O9Y-$)br@V-+L8GBN*|_ zK2cHL|9S&TneqGlP-!6K@P)A?#L*;6hDcA0%SCQ&5-}yI>9>Hkcz=aZs#^0XHKrq7%+?buuGVc{^oiH?C)|*z3DQ>W=X>U1x@M%uDAqG1kUXu z$DIl{h%L+_`e@Xv$jn$Tn3PtPC`ZDO#n;R#$22y#X`|JEkY@zMCKACf-l|ty8s8qG);&&`m{&pj`5ZG<|dOtZ;a;#v7!ore%rH! zKrLo^E-VJ@Dth#OIB**IJH(60W;XdItj?>%t0D~KZe!5w<}g7lVhtwZ)U@rE&3r^< zs~GXEsm+NycB<;MJAKjqj-=QZ&A5!lzeHLOFZH$VYju(k>Cw;l3%1q1LUj&ioy&pJ z1s^$`?L_K4Cq`8;&kSQd;gX%%rn>eHR(ePlH6&Z8`XsE5LQ~K6ksnMO9|EVp_cJ-= zh~{&v_G`P?{SBaX5RubCY|~*&@Dt;^Exf?l{Hr_KE$DIAh-f_Oh*$;uts1-PURGP+ zZJfkPSKr9bz7h_`vu3{?1d-SxfM{X7C$@Lpv2(Ie*(|J1Z?hMfOq~7Jl+QQVWh8v9 z+SRWrW%?Lt-vrA-Bv$o0*K&9w&a|Z~?EsshBavkoht1@mjT|))&+WR7>C^tq6#nBu zeIm+|POL6a-1#H?-uPK44y2JOLVc$RmdTgE6&cWU-O`s4654)nZ;nbZusYL-Q>A*R z)q-iOfwOJ(K5nK|THnKrchga}=?Y&!^?lNw)9IITf7FAN18LjzS0y5VWchuv(D7BB zN|C=YXPZ2nM#5>R>pPJ)R!kLP9YxdAyX`5N)LBZ*!Enz!jP^xkZQWBxBkRg#*wLAe z$^tq+9q1yajuQ14Dv5eqH8B`xKIgIN?V9eOo_6l25d>D1pPwnLuo4x@jB7*|)H437 zcExsyEl3-$WTA7K*)nO7)@_hU)d*dGFqlPBhvUX;RrW1IrE8$a+OMI~_0ePQ&#j{} z7KuCvvW(pjj-%C#YpBX>8S2Z?f+_Q=)FWv^YU_i~-D!c_o|)|i)!oa+LSk+ANT)FMsB(GaDVNI&k+GbDO}>9zK;fMtbx$+Z{dCYy!bwcYl|^}YGJ_U6 zi>E@mKv4gp(oxrN-L`sFj)4O`n ztt64E%o15H&a#aOt(Z8@^`u6)`F-@uuzJKa&h8*BX&9m^qTv}4eCI0!8hDReGBWCM&+E!}iwjVY(w;!0BwQSwL z1(f%F89}_wnkC>ZDX|C&?j+y*!p{XSgPI&R;m6Ny?sI)&m^^ay)Hnzd5|MNv5Qs;t zF4{>9U7x)$$1J-(X4%a#%dU=Df;P^#|Nl>KpXVk<-#04%?>BF#QmucRt9QB)JXiOM z|ME*7^h<4)h!o2?p9#}h)Sd3fKV;9IIA6tb+as(Vf+w$h{L^pTr6-MTGRqp^g2DrX;e$?tS5K@0CM3))Y6sUuZN41+Yp1t8r-z^OB2r0QhoUVh8|#P;ypzxn@v_=)7ss@}JhKE<-8$Cv%QJx>;h z`Buq1mvG}}vaO%$b*KuNEa+LMEM+9b62RatVC7LvY(2tp^V@HK@>El`c~qa9Z&~z{ z%_+6N{V)Ij-#tmkebf+KTX*#h`=v~!t|<_HhC+j72C6fAE9ie1r>XM?GIunZ;xD^O z(vs$HZ@>M;|M#(f`|Yokgbfo+FL+75Zl9?vmY?2yRxVXiW4WB_afa2?a%%YxZQ1ir zRr@d4@beI$Ench~brKf?z!Uj8}AZ3SX>&{btBmtMqJVTDY+ zRAxeJVW@YiB86!U9Uhnc^zPS1e-8BY-tsin!pSMR8`{01|)I+$`#sd?kAY>NhU4+u?Hh`!D zV#!CxWEvr2?@3JaEtvltNz9S=DijVonZxXWjWn$Z2AbJKBTqzvaS30GXo5%54+NkB z>Oao$S2Q0k@y*U9KiIj*Yn_Wc)w$$%I+y%NCz)P!yFD7x*Dtx*F0d0zWtA*aZJ{j~ zdB`^GCwn`|K1e`mDzTjRT*Z=T2^%3njDJA#cI=xOWzR1c zXyq5bX#BUB*B81N^RoaoTk*u>tR{2B(7G$k)fq|S^(apBg3f{R*{GtBF6k1z-|#9Zh!LnoaWQ{stBQoezR<$WP- zgjwAY!#ylR*)A;8mJ2D`pucH3v!q!b8BB0kf!-K$mPWJGj7xlIkH1OL$^K)$k9Cqo zTat<_32vl^{o*w)MpE}LV;oN|%Rm2FkJMjWHiElf6VeJUm{g?pCJ5Vkr{5*ppJ}PQ z)Tfr$;bdq6b^*Dc|M&liCQ3owwiR_YbpIko-WyHqJrh&zO&i`M%S0~iMBu(vB=hDQ z<4}5a7UlCs7ygbsSE|RMM#a$!O~+s=OW4I9I%tt$Ffl*QaH1@%s?@;dnq7|+S)8#1 zCP)8U%BpYQGLW+^JL#+sV(1VbhtAn~3D;)sI$>)O%gmYI^s- z%{30#Z_CZ)J*(yJ@f}BVCBWuUZVPd1aN#LcoG{aiKk+%@1w?n3djmZjfxO=Ih-YNP zw{QVGASpG5S%S(#Y~xChE{n60Nv_YoiD>J>V|ts5#b;KMt6y?XhOATh*j7`O7GVL< zWWPPX$Ln4+efOvrxkQFM3I_vB2m)~v6K7^LXK~U%$_j`X-XXx349E z`-;EGjZ44DA$EQN=i=}%CDt2d)59ZaZjXw0j(L|W0a*xf|*RQhT3=vk1|nfeMV4#o@$d$&0IACgME}ak-2<@>6Oy$TctzOcUwP^wKu{ zgsZM>$|>yA8mvEW5af>G_?blb!7?S7>s#O4Xf+lW_5t1V5Yo%OO5e;)@DnATm;MeN zF$1+m>Omxofa(%^nNRelcJ}H1HJjk3AY+$)Q+<&esli`aw34|`?XbU(?tJe=2sAPh ze?k&+KsxlmVPJ5(%_Fr_$q)oQn1JABSmyOvoQ;euL6wKn{tn$K0aHNq!XHI=3& z#Bpty0oxq+VV{^n`T;pHoto+aYa?I_ZuH}joE;~cx4ue}#4HcvB}T3# z3ZsitSSeKEOvlPx?1#opRjn;nElAadyV!VdpMg}7;;?AsN6=`h6Ni~V>^iYbOy1|I^(N)AdL=A+TXa_M6n3jzH4me-h7XrGDDZFy9KvkOfoFzOL8$dYc zVIywTdLj;p(;QMr8sYNxxrf(B@+m{eM33qF6cKm#>zf&xgQsNSMt2dCKt}@L)##p* zOhTAM!aW8&>39*;_H&kYW4Kc({r5i$=9a*wcd0gS)z3MQ6iKfDp8Jg0W$R3c$zA zl*muip}PQ!ODGTh;HP$=iTX5nE?mqS$6fqSQ4{dCe75q8j0gF3v1L}RcaBYWhE>lD zTG^IaFHYWw$mKFOc7u6V_2G?Ts@vdv$L;UAuPNqpI;$flor}P_-ewR6qZ3-tw8}I-g|CerWAruYJyw)Oil_W#nc#QwK8Arv1Eh z)kZzWynSP_2{KK7j-K|n-+#Gud|$ zH+Wl~c|PaO((K;;>)(EQ`|D3{EB*Mg{wsmM{)PVk@wfkKej~$w(jPYc%{!cb8n&vI zHjuY?+b*ffY@}MX@v+=);b}iAz3oo_v5`dK{4BIQp){y~~6nofYof!R>K(=l6#b ziEm;q)lFg8f(S6x$_+P{w?_)|5&Sd|ybJ_>YQj{{t$XM$1Lzm?#6Vuq4qbka(vuSl z>|{6b_CgvXgaJ1&zbEA4k9-1g6!Dww5%z2S8Z@@1t*85dxu!ajJ(0#S_$gyJJAT_n zSyFj(dzH)#9cIsNQgt5nHE({30!lZX`ua~~m>~0%UC_-L38MZ-wot0^3G&R<8)_Zm zRkbLBu}O{ZzLh%AcpMq^HD1Oc6a&dkew3y=!g;#~L1_{jc-BiMnRyVJ!;UpSbNPFs zVgT}p=Qg@?EtkDLyetwwxXBX$ife4F(Wj=T#tVE zskdaBlfrUv^61fyfsq$^d%(kGq6GpQpoI%$6!PouY?LbrTi#O`w!Ehh<_Q6(SpuUy zowH?>X4Y>7lif%G!}RWL!yN^ZKTc&d-QmqxLP3X7Fs0Tw-DPs>fvT62boJ9+eDxX1 z_?Lv?l;e$Y?wSL@tcVPRPgo~6bGhc?%VFCjM(o7$TR|)U2SodLlcyZ*9WwIipD&P_pqj(;r|TteZ2U9-O7osKb!8*qT-J*$tVo&yRlo%t!pOb- zo;s)S4gTia{onjL;cx%y>{r|uJ`O_~y{qq0($4|dh)BE46CjC~)PS-zvLb0U+4&*{#+`@zwZ=g_^rES zV&8+&``-M$A?f;Ge)_v#v-?n^Txk49A<8cTf`%tN7m+ z$`T6BUHA{ZEitEn9hD6Uj&p5rGykDS1%CSFuYdUUxBqYW`P)z9Prv^3Yvun{C+f8L z*Y{ZTTZg|s=lQpbKMVi50I82;!`J%L=s%5jxX5@;vd6zN;A`2;r?n9SNnU_4arBKJ z(CDY{Yqsmf>m+qT=JAwzoP=_Y6A@=XY>X|H&GN~t-RBA6uk7rXz-}my6&ds{46*)` z5BhVeh&}nFxt;KtQ=nE15sM$@*d#NOA);!5qk3s15tl}#;v#o!8R3f5d*u6!hHcoO z_J@BpxrdzP;q5W5z)4{~8CO|h2PhNlfJceE zk6}EP@csp_bRESyG41mP>0_9M{>DIw^AX^R{W)IFzb>CSZ!CK5 zgU|2PiOrSL7?PRXLou%7ho=7sL{1+`mpzvI(xn!3en0bd6*6TR+yEAK-_WjPTn#aX zjCiO0>u~-mND}|fyY|5#dWtgUrMn=AqgJ9D8R>T!?vRAGR&D6~PU4!r@#MDbnTFIz z$K|FFd2^7}O14u<5@_jj_N=;^78?rGp z4R7d!nu}Q3nFvEIFhQwFG}BHtJfs6`&Un>gC|0ZNBLw(>Twduab4&uQgp#IqHtS3z zEa5aCPhFvzkow^vXyf@c5VZLej3M8flN%*6gfKq|r-nvmHM#-w4{;!+8?i5s2=*#D z+fBp+B~6iuT8v?63Fal3zQ*t=G$c3&Vgzl4byq5~I4$b0&0~B@YbsAjA~fy`EdAW>S&L-v^i*#IMy^T54O*Ggp{L zl8X*zsoV@y9tK9D&)Br%#7RRYCi~v!s-^0ssgKf@$+@14*In}p0T6zLw98qpq%qQyo80E*IC*B-aHqUH0Vg!k<$;US--FFb@QmS|(AK`n*eW0#K2QlteEOx=9NuF{ND$ z`nho^(gw0#iD=Zlmr^7VV65Jkun%&n=l#4z!@c3jdob2w;tiB&&R^Y|PE1WQX0JB! zYU{+r4&!j_BXM=Q>#QYq>6ZgUXqNPKpvmpYaLxCjK|E~v0O+$Y+po6 zSnI*QJTNl}qy437Yq}3ML1tcPvoNjW0#grldR;Ivt7%e4?}upqF15FOJtVLsRsxQ; zZ_BR3AO`24o~z>-M*#OCZ)*Fp2C%Bg_=Rh&%vkf703k5}fwegh+46IpdU{gkYIQ!d z4ZrqrW+l#0PUUkVmNxPDMt8T+r94EMv2D!6?WUw7XUZY4IPFiQ9d9C?1{s3WH#cI^ z+(C29WRa{H0lk@-5^=3)3BRQ7ne{Lq$5S2A_ zjgje*-SwpZXl`_KnsG`FVL|T7#YFB`h}drG!~d?_4Lk*)01lMi5^>#d4oRH{D0B)Z zJ4Y4*X57KiGNdttn~@#5mMYZuG>%Ay67J0(yl zXol+-CE-@tLD3mcCm-nu@m1uKHt-eMldbW~JjQ$0UxetI)Jd|nR^X5676Y8PvBHK; zZdbpRaZt7>&$pp}qv_`Ob{j#=al&~7AZWu*s;!nVh9RnnHIT4r(F%hmtt5uj*5wH* z+JoZ$59LDB*A8W>FjJ%maY64H+O=zFb$gjgkB?VbW)uUOY447@)C<%LH3yJRLY5Vz zLq(S%wlj32ON}8*L<)UV-HNx0D0MX(m*ZPCU6oumDVZ%(7w9s;WqVLmy02P+u1UU@ z$=;>j7HfSs^h@>3OC^n|L%Z8oXINNe)ZIsz%p?s2gG59# zA)#V|4Cm#Z#-}*HL~LrZ>ZszGLx!f5%cY((&>| z(>+e|7Mf_v2f>X@yN;2VrM|@&P4!!QX&eJ$PDE$C+P;1#m)^tY6(<7Z>I)~ zr1}6(W-tOjyp?@2Cv;H~mBYX?hXy#}hW-rQ2E!0*VG0jNR(p%K(b?|E9%D65`)5dy zvhe5oRf^QvE?|XaqN78zOEqGtZgZm|DA7yxyVAv-EI^`G7jIf4+~LjRkgK?5CveB$!eu(bgwN<8wLUHd-yHS zXUKbD4)q*N{^whmA3xuG?&Ew@BYX?<73Wquo))xH-z4|Kz~>9jv>)ytK*4HZSjB^c z5GP?gPK0^WraTJ|#D?02wKnIr`5q$+3)1zm7gx)?$02neMe5>&xgV&jzC1#bK4OyU zfx99~WcbSEM7X(}6Z5a!esJaCPz=(b47x4zh2`EtMWE?jvaB0?24&zGVlRGzVfsrM zodGkf^N5Mq?SuoyrR92vrfY%=_((8#$b|bR&;zfgZ=TN?t~JaspJ|w=$vH?OBxPTJ zp`050yFt(4my*yoJjajFY8SN>O4hasOYp;UDgj7@ghCn+_9S5BDbQ*8PSW=xt-I!X zNbIhWwF)Fn==&6Wp9htv6(9L<`L?oFw5xE0#l~}|Y zr1*Dzh!K<}9r9o=r;C*H);BYUbSqA>U~Vg4+M?IA3*97*MiO-`qM(?x8y~iUvZXUa zNX_kATV;QfRK$xZEP;urU2;+;K3Q~+zqOLkuBnB&`Liu}Vwq2LBWZ>SdO;0}9`>-( zLtsdTu+l@JHz{)u$vmS&y=L}owjJiNMJd{odBM&vHZFRYjLHxLKEA}iN-(#_3hlc+ zwn2JYdzPc;KD~bSovs3k7rmQg5KH0=J2(}WMSl*vG|5qVpC9>$-|`ZXEcyD7^FhIb zjjQC?UCJg}xrK&lNJ1em2z%-?@+80KQj6(u&yB{t!5mH~I@@5T-kXqP)M&4-cNHIw|XDCqDrGf?|g7KKZnJ3XWqm0pIOrGeJxs}HMHS;tV>+Lai(#< z`N<$SAdEY4mtv5`;0&hry+VD64k5PYfhMi#=W1i=?H1QsDd9~m6H%iU%12>>%c&A! zH7N;cDODNDDN|me8GK{5$x?i{G9B&0?mk zIw>y0avj2oGo$t|eTZ6zkg@$i?st4CB<&SavkQUjjzR1~txv=(d%6V_rN`HPo_)iq z0BjWY=2nFAaV!*%GTaXjcITt4j3@N0i=K5u%+L-T3ZeLZO!)6*qKEZoj=~EZYA}LG(5og7Oux^3u_p(vv<9gmylUuu)N3$?dxa;NX4m{Eyp^ zXNUjr>#8_&{-EQ=`mAse@Z5aDvchM@hdusP<#!BZCh+O^uy;<$7)B$#?R?(Ca#_MT z+|MH1PYm}nhMUeN%WB`x|D0Zat6-jtKv*%)L~nvBh=j~d0!6V#>UVg>5?u&%Zsp(9 zb){ha{O`Xo0wwUR^6Re{-As8E;BL8%AjzQezEwqi5yBjy09G!3xF}sGgUw>}lO|bz z-aNnGC@!rxZEtUFJ$q8UO?6+%nouy}zxgEO8}0FP<&U7vy}bE`zYJh{FyVSaC;Pg1 z2xdLTXxn~k%9ZNG?BFwNmMK2iAH3nL{hKxZZ?<|ZMIOxRjC{X_L3d(U?mu8&^{osO z3@+a0l#aA{=BNLUo-B<|I5NPHWv%$4(Yb(xyL>LRlhHEk()u1-lEKAG@3#~tG^`8# zEVQ>= zVcF8}`kIT=6_)O2ggrD7oKn|9ak&Pq<$5qqU5m}awq2Q%sjYtu4ijc^hKOExt?))<9f!FJZt=G-_mjQ34kQ7{4}&zSKRM>P1Q&A+H1a` zH|M8c(JR0j0Rp}tfRF%MK&8KIjbO=7JW9l6@+ox&Q1lZX__{gF4dvWpW}r_>#E9?dG){u3x2hV*44{Kx>~0Z)o;lQ=ApzXdm@≫JO0B!wB6H6G;mg>oazf+f%@7NY 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

fNwU&o$4;I_GsI@-{ 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�+3$H;se>eB><^YkRSF3$Ud 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

C3Yl!Am4zb9Z&^#bSzP3IfR9g{?C3|H& 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 z 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

LpvV&P+Ifg9G!Rc&y&u8` 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 z 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 literal 0 HcmV?d00001 diff --git a/analysis/lang/pl/stempel/row.go b/analysis/lang/pl/stempel/row.go new file mode 100644 index 0000000..04576b4 --- /dev/null +++ b/analysis/lang/pl/stempel/row.go @@ -0,0 +1,80 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "fmt" + + "github.com/blevesearch/stempel/javadata" +) + +type row struct { + cells map[rune]*cell +} + +func (r *row) String() string { + rv := "" + for k, v := range r.cells { + rv += fmt.Sprintf("[%s:%v]\n", string(k), v) + } + return rv +} + +func newRow(r *javadata.Reader) (*row, error) { + rv := &row{ + cells: make(map[rune]*cell), + } + + nCells, err := r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading num cells: %v", err) + } + + for nCells > 0 { + + c, err := r.ReadCharAsRune() + if err != nil { + return nil, fmt.Errorf("error reading cell char: %v", err) + } + cell, err := newCell(r) + if err != nil { + return nil, fmt.Errorf("error reading cell: %v", err) + } + + rv.cells[c] = cell + nCells-- + } + return rv, nil +} + +func (r *row) getCmd(way rune) int32 { + c := r.at(way) + if c != nil { + return c.cmd + } + return -1 +} + +func (r *row) getRef(way rune) int32 { + c := r.at(way) + if c != nil { + return c.ref + } + return -1 +} + +func (r *row) at(c rune) *cell { + return r.cells[c] +} diff --git a/analysis/lang/pl/stempel/strenum.go b/analysis/lang/pl/stempel/strenum.go new file mode 100644 index 0000000..5e00fe9 --- /dev/null +++ b/analysis/lang/pl/stempel/strenum.go @@ -0,0 +1,48 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "io" +) + +type strEnum struct { + r []rune + from int + by int +} + +func newStrEnum(s []rune, up bool) *strEnum { + rv := &strEnum{ + r: s, + } + if up { + rv.from = 0 + rv.by = 1 + } else { + rv.from = len(s) - 1 + rv.by = -1 + } + return rv +} + +func (s *strEnum) next() (rune, error) { + if s.from < 0 || s.from >= len(s.r) { + return 0, io.EOF + } + rv := s.r[s.from] + s.from += s.by + return rv, nil +} diff --git a/analysis/lang/pl/stempel/strenum_test.go b/analysis/lang/pl/stempel/strenum_test.go new file mode 100644 index 0000000..2d9b03d --- /dev/null +++ b/analysis/lang/pl/stempel/strenum_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "fmt" + "io" + "reflect" + "testing" +) + +func TestStrenumNext(t *testing.T) { + + tests := []struct { + in []rune + up bool + expect []rune + }{ + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + up: true, + expect: []rune{'h', 'e', 'l', 'l', 'o'}, + }, + { + in: []rune{'h', 'e', 'l', 'l', 'o'}, + up: false, + expect: []rune{'o', 'l', 'l', 'e', 'h'}, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s-up-%t", string(test.in), test.up), func(t *testing.T) { + strenum := newStrEnum(test.in, test.up) + var got []rune + next, err := strenum.next() + for err == nil { + got = append(got, next) + next, err = strenum.next() + } + if err != io.EOF { + t.Errorf("next got err: %v", err) + } + if !reflect.DeepEqual(got, test.expect) { + t.Errorf("expected %v, got %v", test.expect, got) + } + }) + } + +} diff --git a/analysis/lang/pl/stempel/trie.go b/analysis/lang/pl/stempel/trie.go new file mode 100644 index 0000000..df423bc --- /dev/null +++ b/analysis/lang/pl/stempel/trie.go @@ -0,0 +1,132 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stempel + +import ( + "fmt" + + "github.com/blevesearch/stempel/javadata" +) + +// trie represents the internal trie structure +type trie struct { + rows []*row + cmds []string + root int32 + forward bool +} + +func newTrie(r *javadata.Reader) (rv *trie, err error) { + rv = &trie{} + rv.forward, err = r.ReadBool() + if err != nil { + return nil, fmt.Errorf("error reading trie forward: %v", err) + } + rv.root, err = r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading trie root: %v", err) + } + + // commands + nCommands, err := r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading trie num commands: %v", err) + } + for nCommands > 0 { + utfCommand, nerr := r.ReadUTF() + if nerr != nil { + return nil, fmt.Errorf("error reading trie command utf: %v", nerr) + } + rv.cmds = append(rv.cmds, utfCommand) + nCommands-- + } + + // rows + nRows, err := r.ReadInt32() + if err != nil { + return nil, fmt.Errorf("error reading trie num rows: %v", err) + } + for nRows > 0 { + row, err := newRow(r) + if err != nil { + return nil, fmt.Errorf("error reading trie row: %v", err) + } + rv.rows = append(rv.rows, row) + nRows-- + } + + return rv, nil +} + +func (t *trie) getRow(i int) *row { + if i < 0 || i >= len(t.rows) { + return nil + } + return t.rows[i] +} + +func (t *trie) GetLastOnPath(key []rune) []rune { + now := t.getRow(int(t.root)) + var last []rune + var w int32 + e := newStrEnum(key, t.forward) + + // walk over each rune + // if rune has row in the table, note the cmd (as last) + // if rune has row in table, see if it transitions to another row + // if it does, move to that row and next char on next loop itr + // if it does not, return the last cmd + // if you get to end of string and there is command in row use it + // or return last + for i := 0; i < len(key)-1; i++ { + r, err := e.next() + if err != nil { + return last + } + w = now.getCmd(r) + if w >= 0 { + last = []rune(t.cmds[w]) + } + w = now.getRef(r) + if w >= 0 { + now = t.getRow(int(w)) + } else { + return last + } + } + r, err := e.next() + if err != nil { + return last + } + w = now.getCmd(r) + if err != nil { + return last + } + if w >= 0 { + return []rune(t.cmds[w]) + } + return last +} + +func (t *trie) String() string { + rv := "" + for _, cmd := range t.cmds { + rv += fmt.Sprintf("cmd: %s\n", string(cmd)) + } + for _, row := range t.rows { + rv += fmt.Sprintf("row: %v\n", row) + } + return rv +} diff --git a/analysis/lang/pl/stop_filter_pl.go b/analysis/lang/pl/stop_filter_pl.go new file mode 100644 index 0000000..f8b2918 --- /dev/null +++ b/analysis/lang/pl/stop_filter_pl.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pl/stop_words_pl.go b/analysis/lang/pl/stop_words_pl.go new file mode 100644 index 0000000..0755da8 --- /dev/null +++ b/analysis/lang/pl/stop_words_pl.go @@ -0,0 +1,368 @@ +package pl + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_pl" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var PolishStopWords = []byte(` | From https://github.com/stopwords-iso/stopwords-pl/tree/master + | The MIT License (MIT) + | See https://github.com/stopwords-iso/stopwords-pl/blob/master/LICENSE + | - Encoding was converted to UTF-8. + | - This notice was added. + | - english text is auto-translate + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + | a polish stop word list. comments begin with vertical bar. each stop + | word is at the start of a line. + +a | and +aby | to +ach | ah +acz | although +aczkolwiek | although +aj | ay +albo | or +ale | but +ależ | but +ani | or +aż | until +bardziej | more +bardzo | very +bez | without +bo | because +bowiem | because +by | by +byli | were +bym | i would +bynajmniej | not at all +być | to be +był | was +była | was +było | was +były | were +będzie | will be +będą | they will +cali | inches +cała | whole +cały | whole +chce | i want +choć | though +ci | you +ciebie | you +cię | you +co | what +cokolwiek | whatever +coraz | getting +coś | something +czasami | sometimes +czasem | sometimes +czemu | why +czy | whether +czyli | that is +często | often +daleko | far +dla | for +dlaczego | why +dlatego | which is why +do | down +dobrze | all right +dokąd | where +dość | enough +dr | dr +dużo | a lot +dwa | two +dwaj | two +dwie | two +dwoje | two +dzisiaj | today +dziś | today +gdy | when +gdyby | if +gdyż | because +gdzie | where +gdziekolwiek | wherever +gdzieś | somewhere +go | him +godz | time +hab | hab +i | and +ich | their +ii | ii +iii | iii +ile | how much +im | them +inna | different +inne | other +inny | other +innych | other +inż | eng +iv | iv +ix | ix +iż | that +ja | i +jak | how +jakaś | some +jakby | as if +jaki | what +jakichś | some +jakie | what +jakiś | some +jakiż | what +jakkolwiek | however +jako | as +jakoś | somehow +je | them +jeden | one +jedna | one +jednak | but +jednakże | however +jedno | one +jednym | one +jedynie | only +jego | his +jej | her +jemu | him +jest | is +jestem | i am +jeszcze | still +jeśli | if +jeżeli | if +już | already +ją | i +każdy | everyone +kiedy | when +kierunku | direction +kilka | several +kilku | several +kimś | someone +kto | who +ktokolwiek | anyone +ktoś | someone +która | which +które | which +którego | whose +której | which +który | which +których | which +którym | which +którzy | who +ku | to +lat | years +lecz | but +lub | or +ma | has +mają | may +mam | i have +mamy | we have +mało | little +mgr | msc +mi | to me +miał | had +mimo | despite +między | between +mnie | me +mną | me +mogą | they can +moi | my +moim | my +moja | my +moje | my +może | maybe +możliwe | that's possible +można | you can +mu | him +musi | has to +my | we +mój | my +na | on +nad | above +nam | u.s +nami | us +nas | us +nasi | our +nasz | our +nasza | our +nasze | our +naszego | our +naszych | ours +natomiast | whereas +natychmiast | immediately +nawet | even +nic | nothing +nich | them +nie | no +niech | let +niego | him +niej | her +niemu | not him +nigdy | never +nim | him +nimi | them +nią | her +niż | than +no | yeah +nowe | new +np | e.g. +nr | no +o | about +o.o. | o.o. +obok | near +od | from +ok | approx +około | about +on | he +ona | she +one | they +oni | they +ono | it +oraz | and +oto | here +owszem | yes +pan | mr +pana | mr +pani | you +pl | pl +po | after +pod | under +podczas | while +pomimo | despite +ponad | above +ponieważ | because +powinien | should +powinna | she should +powinni | they should +powinno | should +poza | apart from +prawie | almost +prof | prof +przecież | yet +przed | before +przede | above +przedtem | before +przez | by +przy | by +raz | once +razie | case +roku | year +również | also +sam | alone +sama | alone +się | myself +skąd | from where +sobie | myself +sobą | myself +sposób | way +swoje | own +są | are +ta | this +tak | yes +taka | such +taki | such +takich | such +takie | such +także | too +tam | over there +te | these +tego | this +tej | this one +tel | phone +temu | ago +ten | this +teraz | now +też | too +to | this +tobie | you +tobą | you +toteż | this as well +totobą | you +trzeba | it's necessary to +tu | here +tutaj | here +twoi | yours +twoim | yours +twoja | your +twoje | your +twym | your +twój | your +ty | you +tych | these +tylko | just +tym | this +tys | thousand +tzw | so-called +tę | these +u | at +ul | st +vi | vi +vii | vii +viii | viii +vol | vol +w | in +wam | you +wami | you +was | mustache +wasi | yours +wasz | yours +wasza | yours +wasze | yours +we | in +według | according to +wie | knows +wiele | many +wielu | many +więc | so +więcej | more +wszyscy | all +wszystkich | everyone +wszystkie | all +wszystkim | everyone +wszystko | all +wtedy | then +www | www +wy | you +właśnie | exactly +wśród | among +xi | x.x +xii | xii +xiii | xii +xiv | xiv +xv | xv +z | with +za | behind +zapewne | probably +zawsze | always +zaś | and +ze | that +zeznowu | testify +znowu | again +znów | again +został | left +zł | zloty +żaden | no +żadna | none +żadne | none +żadnych | none +że | that +żeby | to + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(PolishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pt/analyzer_pt.go b/analysis/lang/pt/analyzer_pt.go new file mode 100644 index 0000000..db6b3a7 --- /dev/null +++ b/analysis/lang/pt/analyzer_pt.go @@ -0,0 +1,60 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pt + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "pt" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopPtFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerPtFilter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopPtFilter, + stemmerPtFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pt/analyzer_pt_test.go b/analysis/lang/pt/analyzer_pt_test.go new file mode 100644 index 0000000..417e640 --- /dev/null +++ b/analysis/lang/pt/analyzer_pt_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pt + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestPortugueseAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("quilométricas"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("quilometric"), + }, + }, + }, + { + input: []byte("quilométricos"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("quilometric"), + }, + }, + }, + // stop word + { + input: []byte("não"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/pt/light_stemmer_pt.go b/analysis/lang/pt/light_stemmer_pt.go new file mode 100644 index 0000000..ddb9d16 --- /dev/null +++ b/analysis/lang/pt/light_stemmer_pt.go @@ -0,0 +1,198 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pt + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const LightStemmerName = "stemmer_pt_light" + +type PortugueseLightStemmerFilter struct { +} + +func NewPortugueseLightStemmerFilter() *PortugueseLightStemmerFilter { + return &PortugueseLightStemmerFilter{} +} + +func (s *PortugueseLightStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + runes := bytes.Runes(token.Term) + runes = stem(runes) + token.Term = analysis.BuildTermFromRunes(runes) + } + return input +} + +func stem(input []rune) []rune { + + inputLen := len(input) + + if inputLen < 4 { + return input + } + + input = removeSuffix(input) + inputLen = len(input) + + if inputLen > 3 && input[inputLen-1] == 'a' { + input = normFeminine(input) + inputLen = len(input) + } + + if inputLen > 4 { + switch input[inputLen-1] { + case 'e', 'a', 'o': + input = input[0 : inputLen-1] + inputLen = len(input) + } + } + + for i := 0; i < inputLen; i++ { + switch input[i] { + case 'à', 'á', 'â', 'ä', 'ã': + input[i] = 'a' + case 'ò', 'ó', 'ô', 'ö', 'õ': + input[i] = 'o' + case 'è', 'é', 'ê', 'ë': + input[i] = 'e' + case 'ù', 'ú', 'û', 'ü': + input[i] = 'u' + case 'ì', 'í', 'î', 'ï': + input[i] = 'i' + case 'ç': + input[i] = 'c' + } + } + + return input +} + +func removeSuffix(input []rune) []rune { + + inputLen := len(input) + + if inputLen > 4 && analysis.RunesEndsWith(input, "es") { + switch input[inputLen-3] { + case 'r', 's', 'l', 'z': + return input[0 : inputLen-2] + } + } + + if inputLen > 3 && analysis.RunesEndsWith(input, "ns") { + input[inputLen-2] = 'm' + return input[0 : inputLen-1] + } + + if inputLen > 4 && (analysis.RunesEndsWith(input, "eis") || analysis.RunesEndsWith(input, "éis")) { + input[inputLen-3] = 'e' + input[inputLen-2] = 'l' + return input[0 : inputLen-1] + } + + if inputLen > 4 && analysis.RunesEndsWith(input, "ais") { + input[inputLen-2] = 'l' + return input[0 : inputLen-1] + } + + if inputLen > 4 && analysis.RunesEndsWith(input, "óis") { + input[inputLen-3] = 'o' + input[inputLen-2] = 'l' + return input[0 : inputLen-1] + } + + if inputLen > 4 && analysis.RunesEndsWith(input, "is") { + input[inputLen-1] = 'l' + return input + } + + if inputLen > 3 && + (analysis.RunesEndsWith(input, "ões") || + analysis.RunesEndsWith(input, "ães")) { + input = input[0 : inputLen-1] + inputLen = len(input) + input[inputLen-2] = 'ã' + input[inputLen-1] = 'o' + return input + } + + if inputLen > 6 && analysis.RunesEndsWith(input, "mente") { + return input[0 : inputLen-5] + } + + if inputLen > 3 && input[inputLen-1] == 's' { + return input[0 : inputLen-1] + } + return input +} + +func normFeminine(input []rune) []rune { + inputLen := len(input) + + if inputLen > 7 && + (analysis.RunesEndsWith(input, "inha") || + analysis.RunesEndsWith(input, "iaca") || + analysis.RunesEndsWith(input, "eira")) { + input[inputLen-1] = 'o' + return input + } + + if inputLen > 6 { + if analysis.RunesEndsWith(input, "osa") || + analysis.RunesEndsWith(input, "ica") || + analysis.RunesEndsWith(input, "ida") || + analysis.RunesEndsWith(input, "ada") || + analysis.RunesEndsWith(input, "iva") || + analysis.RunesEndsWith(input, "ama") { + input[inputLen-1] = 'o' + return input + } + + if analysis.RunesEndsWith(input, "ona") { + input[inputLen-3] = 'ã' + input[inputLen-2] = 'o' + return input[0 : inputLen-1] + } + + if analysis.RunesEndsWith(input, "ora") { + return input[0 : inputLen-1] + } + + if analysis.RunesEndsWith(input, "esa") { + input[inputLen-3] = 'ê' + return input[0 : inputLen-1] + } + + if analysis.RunesEndsWith(input, "na") { + input[inputLen-1] = 'o' + return input + } + } + return input +} + +func PortugueseLightStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewPortugueseLightStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(LightStemmerName, PortugueseLightStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pt/light_stemmer_pt_test.go b/analysis/lang/pt/light_stemmer_pt_test.go new file mode 100644 index 0000000..76b1435 --- /dev/null +++ b/analysis/lang/pt/light_stemmer_pt_test.go @@ -0,0 +1,404 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pt + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestPortugueseLightStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("doutores"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("doutor"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("doutor"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("doutor"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("homens"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("homem"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("homem"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("homem"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("papéis"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("papel"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("papel"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("papel"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("normais"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("normal"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("normal"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("normal"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("lencóis"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("lencol"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("lencol"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("lencol"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barris"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barril"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barril"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("barril"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("botões"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bota"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("botão"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bota"), + }, + }, + }, + // longer + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("o"), + }, + &analysis.Token{ + Term: []byte("debate"), + }, + &analysis.Token{ + Term: []byte("político"), + }, + &analysis.Token{ + Term: []byte("pelo"), + }, + &analysis.Token{ + Term: []byte("menos"), + }, + &analysis.Token{ + Term: []byte("o"), + }, + &analysis.Token{ + Term: []byte("que"), + }, + &analysis.Token{ + Term: []byte("vem"), + }, + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("público"), + }, + &analysis.Token{ + Term: []byte("parece"), + }, + &analysis.Token{ + Term: []byte("de"), + }, + &analysis.Token{ + Term: []byte("modo"), + }, + &analysis.Token{ + Term: []byte("nada"), + }, + &analysis.Token{ + Term: []byte("surpreendente"), + }, + &analysis.Token{ + Term: []byte("restrito"), + }, + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("temas"), + }, + &analysis.Token{ + Term: []byte("menores"), + }, + &analysis.Token{ + Term: []byte("mas"), + }, + &analysis.Token{ + Term: []byte("há"), + }, + &analysis.Token{ + Term: []byte("evidentemente"), + }, + &analysis.Token{ + Term: []byte("grandes"), + }, + &analysis.Token{ + Term: []byte("questões"), + }, + &analysis.Token{ + Term: []byte("em"), + }, + &analysis.Token{ + Term: []byte("jogo"), + }, + &analysis.Token{ + Term: []byte("nas"), + }, + &analysis.Token{ + Term: []byte("eleições"), + }, + &analysis.Token{ + Term: []byte("que"), + }, + &analysis.Token{ + Term: []byte("se"), + }, + &analysis.Token{ + Term: []byte("aproximam"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("o"), + }, + &analysis.Token{ + Term: []byte("debat"), + }, + &analysis.Token{ + Term: []byte("politic"), + }, + &analysis.Token{ + Term: []byte("pelo"), + }, + &analysis.Token{ + Term: []byte("meno"), + }, + &analysis.Token{ + Term: []byte("o"), + }, + &analysis.Token{ + Term: []byte("que"), + }, + &analysis.Token{ + Term: []byte("vem"), + }, + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("public"), + }, + &analysis.Token{ + Term: []byte("parec"), + }, + &analysis.Token{ + Term: []byte("de"), + }, + &analysis.Token{ + Term: []byte("modo"), + }, + &analysis.Token{ + Term: []byte("nada"), + }, + &analysis.Token{ + Term: []byte("surpreendent"), + }, + &analysis.Token{ + Term: []byte("restrit"), + }, + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("tema"), + }, + &analysis.Token{ + Term: []byte("menor"), + }, + &analysis.Token{ + Term: []byte("mas"), + }, + &analysis.Token{ + Term: []byte("há"), + }, + &analysis.Token{ + Term: []byte("evident"), + }, + &analysis.Token{ + Term: []byte("grand"), + }, + &analysis.Token{ + Term: []byte("questa"), + }, + &analysis.Token{ + Term: []byte("em"), + }, + &analysis.Token{ + Term: []byte("jogo"), + }, + &analysis.Token{ + Term: []byte("nas"), + }, + &analysis.Token{ + Term: []byte("eleica"), + }, + &analysis.Token{ + Term: []byte("que"), + }, + &analysis.Token{ + Term: []byte("se"), + }, + &analysis.Token{ + Term: []byte("aproximam"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(LightStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/pt/stop_filter_pt.go b/analysis/lang/pt/stop_filter_pt.go new file mode 100644 index 0000000..a0b8886 --- /dev/null +++ b/analysis/lang/pt/stop_filter_pt.go @@ -0,0 +1,36 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pt + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/pt/stop_words_pt.go b/analysis/lang/pt/stop_words_pt.go new file mode 100644 index 0000000..b213389 --- /dev/null +++ b/analysis/lang/pt/stop_words_pt.go @@ -0,0 +1,280 @@ +package pt + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_pt" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var PortugueseStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/portuguese/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A Portuguese stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + + | The following is a ranked list (commonest to rarest) of stopwords + | deriving from a large sample of text. + + | Extra words have been added at the end. + +de | of, from +a | the; to, at; her +o | the; him +que | who, that +e | and +do | de + o +da | de + a +em | in +um | a +para | for + | é from SER +com | with +não | not, no +uma | a +os | the; them +no | em + o +se | himself etc +na | em + a +por | for +mais | more +as | the; them +dos | de + os +como | as, like +mas | but + | foi from SER +ao | a + o +ele | he +das | de + as + | tem from TER +à | a + a +seu | his +sua | her +ou | or + | ser from SER +quando | when +muito | much + | há from HAV +nos | em + os; us +já | already, now + | está from EST +eu | I +também | also +só | only, just +pelo | per + o +pela | per + a +até | up to +isso | that +ela | he +entre | between + | era from SER +depois | after +sem | without +mesmo | same +aos | a + os + | ter from TER +seus | his +quem | whom +nas | em + as +me | me +esse | that +eles | they + | estão from EST +você | you + | tinha from TER + | foram from SER +essa | that +num | em + um +nem | nor +suas | her +meu | my +às | a + as +minha | my + | têm from TER +numa | em + uma +pelos | per + os +elas | they + | havia from HAV + | seja from SER +qual | which + | será from SER +nós | we + | tenho from TER +lhe | to him, her +deles | of them +essas | those +esses | those +pelas | per + as +este | this + | fosse from SER +dele | of him + + | other words. There are many contractions such as naquele = em+aquele, + | mo = me+o, but they are rare. + | Indefinite article plural forms are also rare. + +tu | thou +te | thee +vocês | you (plural) +vos | you +lhes | to them +meus | my +minhas +teu | thy +tua +teus +tuas +nosso | our +nossa +nossos +nossas + +dela | of her +delas | of them + +esta | this +estes | these +estas | these +aquele | that +aquela | that +aqueles | those +aquelas | those +isto | this +aquilo | that + + | forms of estar, to be (not including the infinitive): +estou +está +estamos +estão +estive +esteve +estivemos +estiveram +estava +estávamos +estavam +estivera +estivéramos +esteja +estejamos +estejam +estivesse +estivéssemos +estivessem +estiver +estivermos +estiverem + + | forms of haver, to have (not including the infinitive): +hei +há +havemos +hão +houve +houvemos +houveram +houvera +houvéramos +haja +hajamos +hajam +houvesse +houvéssemos +houvessem +houver +houvermos +houverem +houverei +houverá +houveremos +houverão +houveria +houveríamos +houveriam + + | forms of ser, to be (not including the infinitive): +sou +somos +são +era +éramos +eram +fui +foi +fomos +foram +fora +fôramos +seja +sejamos +sejam +fosse +fôssemos +fossem +for +formos +forem +serei +será +seremos +serão +seria +seríamos +seriam + + | forms of ter, to have (not including the infinitive): +tenho +tem +temos +tém +tinha +tínhamos +tinham +tive +teve +tivemos +tiveram +tivera +tivéramos +tenha +tenhamos +tenham +tivesse +tivéssemos +tivessem +tiver +tivermos +tiverem +terei +terá +teremos +terão +teria +teríamos +teriam +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(PortugueseStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ro/analyzer_ro.go b/analysis/lang/ro/analyzer_ro.go new file mode 100644 index 0000000..f138f80 --- /dev/null +++ b/analysis/lang/ro/analyzer_ro.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ro + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "ro" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopRoFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerRoFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopRoFilter, + stemmerRoFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ro/analyzer_ro_test.go b/analysis/lang/ro/analyzer_ro_test.go new file mode 100644 index 0000000..0fe4645 --- /dev/null +++ b/analysis/lang/ro/analyzer_ro_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ro + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestRomanianAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("absenţa"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("absenţ"), + }, + }, + }, + { + input: []byte("absenţi"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("absenţ"), + }, + }, + }, + // stop word + { + input: []byte("îl"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/ro/stemmer_ro.go b/analysis/lang/ro/stemmer_ro.go new file mode 100644 index 0000000..f7efc73 --- /dev/null +++ b/analysis/lang/ro/stemmer_ro.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ro + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/romanian" +) + +const SnowballStemmerName = "stemmer_ro_snowball" + +type RomanianStemmerFilter struct { +} + +func NewRomanianStemmerFilter() *RomanianStemmerFilter { + return &RomanianStemmerFilter{} +} + +func (s *RomanianStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + romanian.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func RomanianStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewRomanianStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, RomanianStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ro/stop_filter_ro.go b/analysis/lang/ro/stop_filter_ro.go new file mode 100644 index 0000000..0932d33 --- /dev/null +++ b/analysis/lang/ro/stop_filter_ro.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ro + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ro/stop_words_ro.go b/analysis/lang/ro/stop_words_ro.go new file mode 100644 index 0000000..8c01a41 --- /dev/null +++ b/analysis/lang/ro/stop_words_ro.go @@ -0,0 +1,260 @@ +package ro + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_ro" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/ +// ` was changed to ' to allow for literal string + +var RomanianStopWords = []byte(`# This file was created by Jacques Savoy and is distributed under the BSD license. +# See http://members.unine.ch/jacques.savoy/clef/index.html. +# Also see http://www.opensource.org/licenses/bsd-license.html +acea +aceasta +această +aceea +acei +aceia +acel +acela +acele +acelea +acest +acesta +aceste +acestea +aceşti +aceştia +acolo +acum +ai +aia +aibă +aici +al +ăla +ale +alea +ălea +altceva +altcineva +am +ar +are +aş +aşadar +asemenea +asta +ăsta +astăzi +astea +ăstea +ăştia +asupra +aţi +au +avea +avem +aveţi +azi +bine +bucur +bună +ca +că +căci +când +care +cărei +căror +cărui +cât +câte +câţi +către +câtva +ce +cel +ceva +chiar +cînd +cine +cineva +cît +cîte +cîţi +cîtva +contra +cu +cum +cumva +curând +curînd +da +dă +dacă +dar +datorită +de +deci +deja +deoarece +departe +deşi +din +dinaintea +dintr +dintre +drept +după +ea +ei +el +ele +eram +este +eşti +eu +face +fără +fi +fie +fiecare +fii +fim +fiţi +iar +ieri +îi +îl +îmi +împotriva +în +înainte +înaintea +încât +încît +încotro +între +întrucât +întrucît +îţi +la +lângă +le +li +lîngă +lor +lui +mă +mâine +mea +mei +mele +mereu +meu +mi +mine +mult +multă +mulţi +ne +nicăieri +nici +nimeni +nişte +noastră +noastre +noi +noştri +nostru +nu +ori +oricând +oricare +oricât +orice +oricînd +oricine +oricît +oricum +oriunde +până +pe +pentru +peste +pînă +poate +pot +prea +prima +primul +prin +printr +sa +să +săi +sale +sau +său +se +şi +sînt +sîntem +sînteţi +spre +sub +sunt +suntem +sunteţi +ta +tăi +tale +tău +te +ţi +ţie +tine +toată +toate +tot +toţi +totuşi +tu +un +una +unde +undeva +unei +unele +uneori +unor +vă +vi +voastră +voastre +voi +voştri +vostru +vouă +vreo +vreun +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(RomanianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ru/analyzer_ru.go b/analysis/lang/ru/analyzer_ru.go new file mode 100644 index 0000000..06092a4 --- /dev/null +++ b/analysis/lang/ru/analyzer_ru.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ru + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "ru" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + tokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopRuFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerRuFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: tokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopRuFilter, + stemmerRuFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ru/analyzer_ru_test.go b/analysis/lang/ru/analyzer_ru_test.go new file mode 100644 index 0000000..38534af --- /dev/null +++ b/analysis/lang/ru/analyzer_ru_test.go @@ -0,0 +1,122 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ru + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestRussianAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("километрах"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("километр"), + }, + }, + }, + { + input: []byte("актеров"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("актер"), + }, + }, + }, + // stop word + { + input: []byte("как"), + output: analysis.TokenStream{}, + }, + // digits safe + { + input: []byte("text 1000"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("text"), + }, + &analysis.Token{ + Term: []byte("1000"), + }, + }, + }, + { + input: []byte("Вместе с тем о силе электромагнитной энергии имели представление еще"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("вмест"), + }, + &analysis.Token{ + Term: []byte("сил"), + }, + &analysis.Token{ + Term: []byte("электромагнитн"), + }, + &analysis.Token{ + Term: []byte("энерг"), + }, + &analysis.Token{ + Term: []byte("имел"), + }, + &analysis.Token{ + Term: []byte("представлен"), + }, + }, + }, + { + input: []byte("Но знание это хранилось в тайне"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("знан"), + }, + &analysis.Token{ + Term: []byte("эт"), + }, + &analysis.Token{ + Term: []byte("хран"), + }, + &analysis.Token{ + Term: []byte("тайн"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/ru/stemmer_ru.go b/analysis/lang/ru/stemmer_ru.go new file mode 100644 index 0000000..e271bb6 --- /dev/null +++ b/analysis/lang/ru/stemmer_ru.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ru + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/russian" +) + +const SnowballStemmerName = "stemmer_ru_snowball" + +type RussianStemmerFilter struct { +} + +func NewRussianStemmerFilter() *RussianStemmerFilter { + return &RussianStemmerFilter{} +} + +func (s *RussianStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + russian.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func RussianStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewRussianStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, RussianStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ru/stemmer_ru_test.go b/analysis/lang/ru/stemmer_ru_test.go new file mode 100644 index 0000000..8d44181 --- /dev/null +++ b/analysis/lang/ru/stemmer_ru_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ru + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSnowballRussianStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("актеров"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("актер"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("километров"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("километр"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/ru/stop_filter_ru.go b/analysis/lang/ru/stop_filter_ru.go new file mode 100644 index 0000000..a7b9fa1 --- /dev/null +++ b/analysis/lang/ru/stop_filter_ru.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ru + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/ru/stop_words_ru.go b/analysis/lang/ru/stop_words_ru.go new file mode 100644 index 0000000..28ed500 --- /dev/null +++ b/analysis/lang/ru/stop_words_ru.go @@ -0,0 +1,270 @@ +package ru + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_ru" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var RussianStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/russian/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | a russian stop word list. comments begin with vertical bar. each stop + | word is at the start of a line. + + | this is a ranked list (commonest to rarest) of stopwords derived from + | a large text sample. + + | letter 'ё' is translated to 'е'. + +и | and +в | in/into +во | alternative form +не | not +что | what/that +он | he +на | on/onto +я | i +с | from +со | alternative form +как | how +а | milder form of 'no' (but) +то | conjunction and form of 'that' +все | all +она | she +так | so, thus +его | him +но | but +да | yes/and +ты | thou +к | towards, by +у | around, chez +же | intensifier particle +вы | you +за | beyond, behind +бы | conditional/subj. particle +по | up to, along +только | only +ее | her +мне | to me +было | it was +вот | here is/are, particle +от | away from +меня | me +еще | still, yet, more +нет | no, there isnt/arent +о | about +из | out of +ему | to him +теперь | now +когда | when +даже | even +ну | so, well +вдруг | suddenly +ли | interrogative particle +если | if +уже | already, but homonym of 'narrower' +или | or +ни | neither +быть | to be +был | he was +него | prepositional form of его +до | up to +вас | you accusative +нибудь | indef. suffix preceded by hyphen +опять | again +уж | already, but homonym of 'adder' +вам | to you +сказал | he said +ведь | particle 'after all' +там | there +потом | then +себя | oneself +ничего | nothing +ей | to her +может | usually with 'быть' as 'maybe' +они | they +тут | here +где | where +есть | there is/are +надо | got to, must +ней | prepositional form of ей +для | for +мы | we +тебя | thee +их | them, their +чем | than +была | she was +сам | self +чтоб | in order to +без | without +будто | as if +человек | man, person, one +чего | genitive form of 'what' +раз | once +тоже | also +себе | to oneself +под | beneath +жизнь | life +будет | will be +ж | short form of intensifer particle 'же' +тогда | then +кто | who +этот | this +говорил | was saying +того | genitive form of 'that' +потому | for that reason +этого | genitive form of 'this' +какой | which +совсем | altogether +ним | prepositional form of 'его', 'они' +здесь | here +этом | prepositional form of 'этот' +один | one +почти | almost +мой | my +тем | instrumental/dative plural of 'тот', 'то' +чтобы | full form of 'in order that' +нее | her (acc.) +кажется | it seems +сейчас | now +были | they were +куда | where to +зачем | why +сказать | to say +всех | all (acc., gen. preposn. plural) +никогда | never +сегодня | today +можно | possible, one can +при | by +наконец | finally +два | two +об | alternative form of 'о', about +другой | another +хоть | even +после | after +над | above +больше | more +тот | that one (masc.) +через | across, in +эти | these +нас | us +про | about +всего | in all, only, of all +них | prepositional form of 'они' (they) +какая | which, feminine +много | lots +разве | interrogative particle +сказала | she said +три | three +эту | this, acc. fem. sing. +моя | my, feminine +впрочем | moreover, besides +хорошо | good +свою | ones own, acc. fem. sing. +этой | oblique form of 'эта', fem. 'this' +перед | in front of +иногда | sometimes +лучше | better +чуть | a little +том | preposn. form of 'that one' +нельзя | one must not +такой | such a one +им | to them +более | more +всегда | always +конечно | of course +всю | acc. fem. sing of 'all' +между | between + + + | b: some paradigms + | + | personal pronouns + | + | я меня мне мной [мною] + | ты тебя тебе тобой [тобою] + | он его ему им [него, нему, ним] + | она ее эи ею [нее, нэи, нею] + | оно его ему им [него, нему, ним] + | + | мы нас нам нами + | вы вас вам вами + | они их им ими [них, ним, ними] + | + | себя себе собой [собою] + | + | demonstrative pronouns: этот (this), тот (that) + | + | этот эта это эти + | этого эты это эти + | этого этой этого этих + | этому этой этому этим + | этим этой этим [этою] этими + | этом этой этом этих + | + | тот та то те + | того ту то те + | того той того тех + | тому той тому тем + | тем той тем [тою] теми + | том той том тех + | + | determinative pronouns + | + | (a) весь (all) + | + | весь вся все все + | всего всю все все + | всего всей всего всех + | всему всей всему всем + | всем всей всем [всею] всеми + | всем всей всем всех + | + | (b) сам (himself etc) + | + | сам сама само сами + | самого саму само самих + | самого самой самого самих + | самому самой самому самим + | самим самой самим [самою] самими + | самом самой самом самих + | + | stems of verbs 'to be', 'to have', 'to do' and modal + | + | быть бы буд быв есть суть + | име + | дел + | мог мож мочь + | уме + | хоч хот + | долж + | можн + | нужн + | нельзя + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(RussianStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/sv/analyzer_sv.go b/analysis/lang/sv/analyzer_sv.go new file mode 100644 index 0000000..e8c76e1 --- /dev/null +++ b/analysis/lang/sv/analyzer_sv.go @@ -0,0 +1,60 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sv + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "sv" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopSvFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerSvFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + toLowerFilter, + stopSvFilter, + stemmerSvFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/sv/analyzer_sv_test.go b/analysis/lang/sv/analyzer_sv_test.go new file mode 100644 index 0000000..a3bd5f1 --- /dev/null +++ b/analysis/lang/sv/analyzer_sv_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sv + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSwedishAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("jaktkarlarne"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("jaktkarl"), + }, + }, + }, + { + input: []byte("jaktkarlens"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("jaktkarl"), + }, + }, + }, + // stop word + { + input: []byte("och"), + output: analysis.TokenStream{}, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/sv/stemmer_sv.go b/analysis/lang/sv/stemmer_sv.go new file mode 100644 index 0000000..679fc98 --- /dev/null +++ b/analysis/lang/sv/stemmer_sv.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sv + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/swedish" +) + +const SnowballStemmerName = "stemmer_sv_snowball" + +type SwedishStemmerFilter struct { +} + +func NewSwedishStemmerFilter() *SwedishStemmerFilter { + return &SwedishStemmerFilter{} +} + +func (s *SwedishStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + swedish.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func SwedishStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewSwedishStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, SwedishStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/sv/stop_filter_sv.go b/analysis/lang/sv/stop_filter_sv.go new file mode 100644 index 0000000..9d3a57d --- /dev/null +++ b/analysis/lang/sv/stop_filter_sv.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sv + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/sv/stop_words_sv.go b/analysis/lang/sv/stop_words_sv.go new file mode 100644 index 0000000..a420e7d --- /dev/null +++ b/analysis/lang/sv/stop_words_sv.go @@ -0,0 +1,160 @@ +package sv + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_sv" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var SwedishStopWords = []byte(` | From svn.tartarus.org/snowball/trunk/website/algorithms/swedish/stop.txt + | This file is distributed under the BSD License. + | See http://snowball.tartarus.org/license.php + | Also see http://www.opensource.org/licenses/bsd-license.html + | - Encoding was converted to UTF-8. + | - This notice was added. + | + | NOTE: To use this file with StopFilterFactory, you must specify format="snowball" + + | A Swedish stop word list. Comments begin with vertical bar. Each stop + | word is at the start of a line. + + | This is a ranked list (commonest to rarest) of stopwords derived from + | a large text sample. + + | Swedish stop words occasionally exhibit homonym clashes. For example + | så = so, but also seed. These are indicated clearly below. + +och | and +det | it, this/that +att | to (with infinitive) +i | in, at +en | a +jag | I +hon | she +som | who, that +han | he +på | on +den | it, this/that +med | with +var | where, each +sig | him(self) etc +för | for +så | so (also: seed) +till | to +är | is +men | but +ett | a +om | if; around, about +hade | had +de | they, these/those +av | of +icke | not, no +mig | me +du | you +henne | her +då | then, when +sin | his +nu | now +har | have +inte | inte någon = no one +hans | his +honom | him +skulle | 'sake' +hennes | her +där | there +min | my +man | one (pronoun) +ej | nor +vid | at, by, on (also: vast) +kunde | could +något | some etc +från | from, off +ut | out +när | when +efter | after, behind +upp | up +vi | we +dem | them +vara | be +vad | what +över | over +än | than +dig | you +kan | can +sina | his +här | here +ha | have +mot | towards +alla | all +under | under (also: wonder) +någon | some etc +eller | or (else) +allt | all +mycket | much +sedan | since +ju | why +denna | this/that +själv | myself, yourself etc +detta | this/that +åt | to +utan | without +varit | was +hur | how +ingen | no +mitt | my +ni | you +bli | to be, become +blev | from bli +oss | us +din | thy +dessa | these/those +några | some etc +deras | their +blir | from bli +mina | my +samma | (the) same +vilken | who, that +er | you, your +sådan | such a +vår | our +blivit | from bli +dess | its +inom | within +mellan | between +sådant | such a +varför | why +varje | each +vilka | who, that +ditt | thy +vem | who +vilket | who, that +sitta | his +sådana | such a +vart | each +dina | thy +vars | whose +vårt | our +våra | our +ert | your +era | your +vilkas | whose + +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(SwedishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/tr/analyzer_tr.go b/analysis/lang/tr/analyzer_tr.go new file mode 100644 index 0000000..69e7537 --- /dev/null +++ b/analysis/lang/tr/analyzer_tr.go @@ -0,0 +1,66 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/bleve/v2/analysis/token/apostrophe" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +) + +const AnalyzerName = "tr" + +func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Analyzer, error) { + unicodeTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + aposFilter, err := cache.TokenFilterNamed(apostrophe.Name) + if err != nil { + return nil, err + } + toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) + if err != nil { + return nil, err + } + stopTrFilter, err := cache.TokenFilterNamed(StopName) + if err != nil { + return nil, err + } + stemmerTrFilter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + return nil, err + } + rv := analysis.DefaultAnalyzer{ + Tokenizer: unicodeTokenizer, + TokenFilters: []analysis.TokenFilter{ + aposFilter, + toLowerFilter, + stopTrFilter, + stemmerTrFilter, + }, + } + return &rv, nil +} + +func init() { + err := registry.RegisterAnalyzer(AnalyzerName, AnalyzerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/tr/analyzer_tr_test.go b/analysis/lang/tr/analyzer_tr_test.go new file mode 100644 index 0000000..3c4592b --- /dev/null +++ b/analysis/lang/tr/analyzer_tr_test.go @@ -0,0 +1,90 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestTurkishAnalyzer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + // stemming + { + input: []byte("ağacı"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ağaç"), + }, + }, + }, + { + input: []byte("ağaç"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ağaç"), + }, + }, + }, + // stop word + { + input: []byte("dolayı"), + output: analysis.TokenStream{}, + }, + // apostrophes + { + input: []byte("Kıbrıs'ta"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kıbrıs"), + }, + }, + }, + { + input: []byte("Van Gölü'ne"), + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("van"), + }, + &analysis.Token{ + Term: []byte("göl"), + }, + }, + }, + } + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(AnalyzerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := analyzer.Analyze(test.input) + if len(actual) != len(test.output) { + t.Fatalf("expected length: %d, got %d", len(test.output), len(actual)) + } + for i, tok := range actual { + if !reflect.DeepEqual(tok.Term, test.output[i].Term) { + t.Errorf("expected term %s (% x) got %s (% x)", test.output[i].Term, test.output[i].Term, tok.Term, tok.Term) + } + } + } +} diff --git a/analysis/lang/tr/stemmer_tr.go b/analysis/lang/tr/stemmer_tr.go new file mode 100644 index 0000000..ce36655 --- /dev/null +++ b/analysis/lang/tr/stemmer_tr.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowballstem" + "github.com/blevesearch/snowballstem/turkish" +) + +const SnowballStemmerName = "stemmer_tr_snowball" + +type TurkishStemmerFilter struct { +} + +func NewTurkishStemmerFilter() *TurkishStemmerFilter { + return &TurkishStemmerFilter{} +} + +func (s *TurkishStemmerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + env := snowballstem.NewEnv(string(token.Term)) + turkish.Stem(env) + token.Term = []byte(env.Current()) + } + return input +} + +func TurkishStemmerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewTurkishStemmerFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(SnowballStemmerName, TurkishStemmerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/tr/stemmer_tr_test.go b/analysis/lang/tr/stemmer_tr_test.go new file mode 100644 index 0000000..8ad5a56 --- /dev/null +++ b/analysis/lang/tr/stemmer_tr_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2025 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tr + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestSnowballTurkishStemmer(t *testing.T) { + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kimsesizler"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kimsesiz"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kitaplar"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kitap"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("arabanın"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("araba"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bardaklar"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("bardak"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kediye"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("kedi"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("yazdım"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("yaz"), + }, + }, + }, + } + + cache := registry.NewCache() + filter, err := cache.TokenFilterNamed(SnowballStemmerName) + if err != nil { + t.Fatal(err) + } + for _, test := range tests { + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/lang/tr/stop_filter_tr.go b/analysis/lang/tr/stop_filter_tr.go new file mode 100644 index 0000000..0f75ffa --- /dev/null +++ b/analysis/lang/tr/stop_filter_tr.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/token/stop" + "github.com/blevesearch/bleve/v2/registry" +) + +func StopTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + tokenMap, err := cache.TokenMapNamed(StopName) + if err != nil { + return nil, err + } + return stop.NewStopTokensFilter(tokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(StopName, StopTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/lang/tr/stop_words_tr.go b/analysis/lang/tr/stop_words_tr.go new file mode 100644 index 0000000..49116c2 --- /dev/null +++ b/analysis/lang/tr/stop_words_tr.go @@ -0,0 +1,239 @@ +package tr + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const StopName = "stop_tr" + +// this content was obtained from: +// lucene-4.7.2/analysis/common/src/resources/org/apache/lucene/analysis/snowball/ +// ` was changed to ' to allow for literal string + +var TurkishStopWords = []byte(`# Turkish stopwords from LUCENE-559 +# merged with the list from "Information Retrieval on Turkish Texts" +# (http://www.users.muohio.edu/canf/papers/JASIST2008offPrint.pdf) +acaba +altmış +altı +ama +ancak +arada +aslında +ayrıca +bana +bazı +belki +ben +benden +beni +benim +beri +beş +bile +bin +bir +birçok +biri +birkaç +birkez +birşey +birşeyi +biz +bize +bizden +bizi +bizim +böyle +böylece +bu +buna +bunda +bundan +bunlar +bunları +bunların +bunu +bunun +burada +çok +çünkü +da +daha +dahi +de +defa +değil +diğer +diye +doksan +dokuz +dolayı +dolayısıyla +dört +edecek +eden +ederek +edilecek +ediliyor +edilmesi +ediyor +eğer +elli +en +etmesi +etti +ettiği +ettiğini +gibi +göre +halen +hangi +hatta +hem +henüz +hep +hepsi +her +herhangi +herkesin +hiç +hiçbir +için +iki +ile +ilgili +ise +işte +itibaren +itibariyle +kadar +karşın +katrilyon +kendi +kendilerine +kendini +kendisi +kendisine +kendisini +kez +ki +kim +kimden +kime +kimi +kimse +kırk +milyar +milyon +mu +mü +mı +nasıl +ne +neden +nedenle +nerde +nerede +nereye +niye +niçin +o +olan +olarak +oldu +olduğu +olduğunu +olduklarını +olmadı +olmadığı +olmak +olması +olmayan +olmaz +olsa +olsun +olup +olur +olursa +oluyor +on +ona +ondan +onlar +onlardan +onları +onların +onu +onun +otuz +oysa +öyle +pek +rağmen +sadece +sanki +sekiz +seksen +sen +senden +seni +senin +siz +sizden +sizi +sizin +şey +şeyden +şeyi +şeyler +şöyle +şu +şuna +şunda +şundan +şunları +şunu +tarafından +trilyon +tüm +üç +üzere +var +vardı +ve +veya +ya +yani +yapacak +yapılan +yapılması +yapıyor +yapmak +yaptı +yaptığı +yaptığını +yaptıkları +yedi +yerine +yetmiş +yine +yirmi +yoksa +yüz +zaten +`) + +func TokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + err := rv.LoadBytes(TurkishStopWords) + return rv, err +} + +func init() { + err := registry.RegisterTokenMap(StopName, TokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/test_words.txt b/analysis/test_words.txt new file mode 100644 index 0000000..b86e254 --- /dev/null +++ b/analysis/test_words.txt @@ -0,0 +1,7 @@ +# full line comment +marty +steve # trailing comment +| different format of comment +dustin +siri | different style trailing comment +multiple words with different whitespace \ No newline at end of file diff --git a/analysis/token/apostrophe/apostrophe.go b/analysis/token/apostrophe/apostrophe.go new file mode 100644 index 0000000..20290a3 --- /dev/null +++ b/analysis/token/apostrophe/apostrophe.go @@ -0,0 +1,57 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apostrophe + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "apostrophe" + +const RightSingleQuotationMark = "’" +const Apostrophe = "'" +const Apostrophes = Apostrophe + RightSingleQuotationMark + +type ApostropheFilter struct{} + +func NewApostropheFilter() *ApostropheFilter { + return &ApostropheFilter{} +} + +func (s *ApostropheFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + firstApostrophe := bytes.IndexAny(token.Term, Apostrophes) + if firstApostrophe >= 0 { + // found an apostrophe + token.Term = token.Term[0:firstApostrophe] + } + } + + return input +} + +func ApostropheFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewApostropheFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, ApostropheFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/apostrophe/apostrophe_test.go b/analysis/token/apostrophe/apostrophe_test.go new file mode 100644 index 0000000..b704dc7 --- /dev/null +++ b/analysis/token/apostrophe/apostrophe_test.go @@ -0,0 +1,99 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apostrophe + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestApostropheFilter(t *testing.T) { + + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Türkiye'de"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Türkiye"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("2003'te"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("2003"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Van"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Van"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Gölü'nü"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Gölü"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("gördüm"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("gördüm"), + }, + }, + }, + } + + for _, test := range tests { + apostropheFilter := NewApostropheFilter() + actual := apostropheFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/token/camelcase/camelcase.go b/analysis/token/camelcase/camelcase.go new file mode 100644 index 0000000..7aab53f --- /dev/null +++ b/analysis/token/camelcase/camelcase.go @@ -0,0 +1,81 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package camelcase + +import ( + "bytes" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "camelCase" + +// CamelCaseFilter splits a given token into a set of tokens where each resulting token +// falls into one the following classes: +// 1. Upper case followed by lower case letters. +// Terminated by a number, an upper case letter, and a non alpha-numeric symbol. +// 2. Upper case followed by upper case letters. +// Terminated by a number, an upper case followed by a lower case letter, and a non alpha-numeric symbol. +// 3. Lower case followed by lower case letters. +// Terminated by a number, an upper case letter, and a non alpha-numeric symbol. +// 4. Number followed by numbers. +// Terminated by a letter, and a non alpha-numeric symbol. +// 5. Non alpha-numeric symbol followed by non alpha-numeric symbols. +// Terminated by a number, and a letter. +// +// It does a one-time sequential pass over an input token, from left to right. +// The scan is greedy and generates the longest substring that fits into one of the classes. +// +// See the test file for examples of classes and their parsings. +type CamelCaseFilter struct{} + +func NewCamelCaseFilter() *CamelCaseFilter { + return &CamelCaseFilter{} +} + +func (f *CamelCaseFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + rv := make(analysis.TokenStream, 0, len(input)) + + nextPosition := 1 + for _, token := range input { + runeCount := utf8.RuneCount(token.Term) + runes := bytes.Runes(token.Term) + + p := NewParser(runeCount, nextPosition, token.Start) + for i := 0; i < runeCount; i++ { + if i+1 >= runeCount { + p.Push(runes[i], nil) + } else { + p.Push(runes[i], &runes[i+1]) + } + } + rv = append(rv, p.FlushTokens()...) + nextPosition = p.NextPosition() + } + return rv +} + +func CamelCaseFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewCamelCaseFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, CamelCaseFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/camelcase/camelcase_test.go b/analysis/token/camelcase/camelcase_test.go new file mode 100644 index 0000000..9220bc5 --- /dev/null +++ b/analysis/token/camelcase/camelcase_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package camelcase + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestCamelCaseFilter(t *testing.T) { + + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: tokenStream(""), + output: tokenStream(""), + }, + { + input: tokenStream("a"), + output: tokenStream("a"), + }, + + { + input: tokenStream("...aMACMac123macILoveGolang"), + output: tokenStream("...", "a", "MAC", "Mac", "123", "mac", "I", "Love", "Golang"), + }, + { + input: tokenStream("Lang"), + output: tokenStream("Lang"), + }, + { + input: tokenStream("GLang"), + output: tokenStream("G", "Lang"), + }, + { + input: tokenStream("GOLang"), + output: tokenStream("GO", "Lang"), + }, + { + input: tokenStream("GOOLang"), + output: tokenStream("GOO", "Lang"), + }, + { + input: tokenStream("1234"), + output: tokenStream("1234"), + }, + { + input: tokenStream("starbucks"), + output: tokenStream("starbucks"), + }, + { + input: tokenStream("Starbucks TVSamsungIsGREAT000"), + output: tokenStream("Starbucks", " ", "TV", "Samsung", "Is", "GREAT", "000"), + }, + } + + for _, test := range tests { + ccFilter := NewCamelCaseFilter() + actual := ccFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s \n\n got %s", test.output, actual) + } + } +} + +func tokenStream(termStrs ...string) analysis.TokenStream { + tokenStream := make([]*analysis.Token, len(termStrs)) + index := 0 + for i, termStr := range termStrs { + tokenStream[i] = &analysis.Token{ + Term: []byte(termStr), + Position: i + 1, + Start: index, + End: index + len(termStr), + } + index += len(termStr) + } + return analysis.TokenStream(tokenStream) +} diff --git a/analysis/token/camelcase/parser.go b/analysis/token/camelcase/parser.go new file mode 100644 index 0000000..2d93fc4 --- /dev/null +++ b/analysis/token/camelcase/parser.go @@ -0,0 +1,109 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package camelcase + +import ( + "github.com/blevesearch/bleve/v2/analysis" +) + +func (p *Parser) buildTokenFromTerm(buffer []rune) *analysis.Token { + term := analysis.BuildTermFromRunes(buffer) + token := &analysis.Token{ + Term: term, + Position: p.position, + Start: p.index, + End: p.index + len(term), + } + p.position++ + p.index += len(term) + return token +} + +// Parser accepts a symbol and passes it to the current state (representing a class). +// The state can accept it (and accumulate it). Otherwise, the parser creates a new state that +// starts with the pushed symbol. +// +// Parser accumulates a new resulting token every time it switches state. +// Use FlushTokens() to get the results after the last symbol was pushed. +type Parser struct { + bufferLen int + buffer []rune + current State + tokens []*analysis.Token + position int + index int +} + +func NewParser(length, position, index int) *Parser { + return &Parser{ + bufferLen: length, + buffer: make([]rune, 0, length), + tokens: make([]*analysis.Token, 0, length), + position: position, + index: index, + } +} + +func (p *Parser) Push(sym rune, peek *rune) { + if p.current == nil { + // the start of parsing + p.current = p.NewState(sym) + p.buffer = append(p.buffer, sym) + + } else if p.current.Member(sym, peek) { + // same state, just accumulate + p.buffer = append(p.buffer, sym) + + } else { + // the old state is no more, thus convert the buffer + p.tokens = append(p.tokens, p.buildTokenFromTerm(p.buffer)) + + // let the new state begin + p.current = p.NewState(sym) + p.buffer = make([]rune, 0, p.bufferLen) + p.buffer = append(p.buffer, sym) + } +} + +// Note. States have to have different starting symbols. +func (p *Parser) NewState(sym rune) State { + var found State + + found = &LowerCaseState{} + if found.StartSym(sym) { + return found + } + + found = &UpperCaseState{} + if found.StartSym(sym) { + return found + } + + found = &NumberCaseState{} + if found.StartSym(sym) { + return found + } + + return &NonAlphaNumericCaseState{} +} + +func (p *Parser) FlushTokens() []*analysis.Token { + p.tokens = append(p.tokens, p.buildTokenFromTerm(p.buffer)) + return p.tokens +} + +func (p *Parser) NextPosition() int { + return p.position +} diff --git a/analysis/token/camelcase/states.go b/analysis/token/camelcase/states.go new file mode 100644 index 0000000..97e08d0 --- /dev/null +++ b/analysis/token/camelcase/states.go @@ -0,0 +1,87 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package camelcase + +import ( + "unicode" +) + +// States codify the classes that the parser recognizes. +type State interface { + // is _sym_ the start character + StartSym(sym rune) bool + + // is _sym_ a member of a class. + // peek, the next sym on the tape, can also be used to determine a class. + Member(sym rune, peek *rune) bool +} + +type LowerCaseState struct{} + +func (s *LowerCaseState) Member(sym rune, peek *rune) bool { + return unicode.IsLower(sym) +} + +func (s *LowerCaseState) StartSym(sym rune) bool { + return s.Member(sym, nil) +} + +type UpperCaseState struct { + startedCollecting bool // denotes that the start character has been read + collectingUpper bool // denotes if this is a class of all upper case letters +} + +func (s *UpperCaseState) Member(sym rune, peek *rune) bool { + if !(unicode.IsLower(sym) || unicode.IsUpper(sym)) { + return false + } + + if peek != nil && unicode.IsUpper(sym) && unicode.IsLower(*peek) { + return false + } + + if !s.startedCollecting { + // now we have to determine if upper-case letters are collected. + s.startedCollecting = true + s.collectingUpper = unicode.IsUpper(sym) + return true + } + + return s.collectingUpper == unicode.IsUpper(sym) +} + +func (s *UpperCaseState) StartSym(sym rune) bool { + return unicode.IsUpper(sym) +} + +type NumberCaseState struct{} + +func (s *NumberCaseState) Member(sym rune, peek *rune) bool { + return unicode.IsNumber(sym) +} + +func (s *NumberCaseState) StartSym(sym rune) bool { + return s.Member(sym, nil) +} + +type NonAlphaNumericCaseState struct{} + +func (s *NonAlphaNumericCaseState) Member(sym rune, peek *rune) bool { + return !unicode.IsLower(sym) && !unicode.IsUpper(sym) && !unicode.IsNumber(sym) +} + +func (s *NonAlphaNumericCaseState) StartSym(sym rune) bool { + return s.Member(sym, nil) +} diff --git a/analysis/token/compound/dict.go b/analysis/token/compound/dict.go new file mode 100644 index 0000000..f4fc8f5 --- /dev/null +++ b/analysis/token/compound/dict.go @@ -0,0 +1,144 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compound + +import ( + "bytes" + "fmt" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "dict_compound" + +const defaultMinWordSize = 5 +const defaultMinSubWordSize = 2 +const defaultMaxSubWordSize = 15 +const defaultOnlyLongestMatch = false + +type DictionaryCompoundFilter struct { + dict analysis.TokenMap + minWordSize int + minSubWordSize int + maxSubWordSize int + onlyLongestMatch bool +} + +func NewDictionaryCompoundFilter(dict analysis.TokenMap, minWordSize, minSubWordSize, maxSubWordSize int, onlyLongestMatch bool) *DictionaryCompoundFilter { + return &DictionaryCompoundFilter{ + dict: dict, + minWordSize: minWordSize, + minSubWordSize: minSubWordSize, + maxSubWordSize: maxSubWordSize, + onlyLongestMatch: onlyLongestMatch, + } +} + +func (f *DictionaryCompoundFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + rv := make(analysis.TokenStream, 0, len(input)) + + for _, token := range input { + rv = append(rv, token) + tokenLen := utf8.RuneCount(token.Term) + if tokenLen >= f.minWordSize { + newtokens := f.decompose(token) + for _, newtoken := range newtokens { + rv = append(rv, newtoken) + } + } + } + + return rv +} + +func (f *DictionaryCompoundFilter) decompose(token *analysis.Token) []*analysis.Token { + runes := bytes.Runes(token.Term) + rv := make([]*analysis.Token, 0) + rlen := len(runes) + for i := 0; i <= (rlen - f.minSubWordSize); i++ { + var longestMatchToken *analysis.Token + for j := f.minSubWordSize; j <= f.maxSubWordSize; j++ { + if i+j > rlen { + break + } + _, inDict := f.dict[string(runes[i:i+j])] + if inDict { + newtoken := analysis.Token{ + Term: []byte(string(runes[i : i+j])), + Position: token.Position, + Start: token.Start + i, + End: token.Start + i + j, + Type: token.Type, + KeyWord: token.KeyWord, + } + if f.onlyLongestMatch { + if longestMatchToken == nil || utf8.RuneCount(longestMatchToken.Term) < j { + longestMatchToken = &newtoken + } + } else { + rv = append(rv, &newtoken) + } + } + } + if f.onlyLongestMatch && longestMatchToken != nil { + rv = append(rv, longestMatchToken) + } + } + return rv +} + +func DictionaryCompoundFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + + minWordSize := defaultMinWordSize + minSubWordSize := defaultMinSubWordSize + maxSubWordSize := defaultMaxSubWordSize + onlyLongestMatch := defaultOnlyLongestMatch + + minVal, ok := config["min_word_size"].(float64) + if ok { + minWordSize = int(minVal) + } + minSubVal, ok := config["min_subword_size"].(float64) + if ok { + minSubWordSize = int(minSubVal) + } + maxSubVal, ok := config["max_subword_size"].(float64) + if ok { + maxSubWordSize = int(maxSubVal) + } + onlyVal, ok := config["only_longest_match"].(bool) + if ok { + onlyLongestMatch = onlyVal + } + + dictTokenMapName, ok := config["dict_token_map"].(string) + if !ok { + return nil, fmt.Errorf("must specify dict_token_map") + } + dictTokenMap, err := cache.TokenMapNamed(dictTokenMapName) + if err != nil { + return nil, fmt.Errorf("error building dict compound words filter: %v", err) + } + return NewDictionaryCompoundFilter(dictTokenMap, minWordSize, minSubWordSize, maxSubWordSize, onlyLongestMatch), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, DictionaryCompoundFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/compound/dict_test.go b/analysis/token/compound/dict_test.go new file mode 100644 index 0000000..8d9b093 --- /dev/null +++ b/analysis/token/compound/dict_test.go @@ -0,0 +1,187 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compound + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenmap" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestStopWordsFilter(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("i"), + Start: 0, + End: 1, + Position: 1, + }, + &analysis.Token{ + Term: []byte("like"), + Start: 2, + End: 6, + Position: 2, + }, + &analysis.Token{ + Term: []byte("to"), + Start: 7, + End: 9, + Position: 3, + }, + &analysis.Token{ + Term: []byte("play"), + Start: 10, + End: 14, + Position: 4, + }, + &analysis.Token{ + Term: []byte("softball"), + Start: 15, + End: 23, + Position: 5, + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("i"), + Start: 0, + End: 1, + Position: 1, + }, + &analysis.Token{ + Term: []byte("like"), + Start: 2, + End: 6, + Position: 2, + }, + &analysis.Token{ + Term: []byte("to"), + Start: 7, + End: 9, + Position: 3, + }, + &analysis.Token{ + Term: []byte("play"), + Start: 10, + End: 14, + Position: 4, + }, + &analysis.Token{ + Term: []byte("softball"), + Start: 15, + End: 23, + Position: 5, + }, + &analysis.Token{ + Term: []byte("soft"), + Start: 15, + End: 19, + Position: 5, + }, + &analysis.Token{ + Term: []byte("ball"), + Start: 19, + End: 23, + Position: 5, + }, + } + + cache := registry.NewCache() + dictListConfig := map[string]interface{}{ + "type": tokenmap.Name, + "tokens": []interface{}{"factor", "soft", "ball", "team"}, + } + _, err := cache.DefineTokenMap("dict_test", dictListConfig) + if err != nil { + t.Fatal(err) + } + + dictConfig := map[string]interface{}{ + "type": "dict_compound", + "dict_token_map": "dict_test", + } + dictFilter, err := cache.DefineTokenFilter("dict_test", dictConfig) + if err != nil { + t.Fatal(err) + } + + ouputTokenStream := dictFilter.Filter(inputTokenStream) + if !reflect.DeepEqual(ouputTokenStream, expectedTokenStream) { + t.Errorf("expected %#v got %#v", expectedTokenStream, ouputTokenStream) + } +} + +func TestStopWordsFilterLongestMatch(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("softestball"), + Start: 0, + End: 11, + Position: 1, + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("softestball"), + Start: 0, + End: 11, + Position: 1, + }, + &analysis.Token{ + Term: []byte("softest"), + Start: 0, + End: 7, + Position: 1, + }, + &analysis.Token{ + Term: []byte("ball"), + Start: 7, + End: 11, + Position: 1, + }, + } + + cache := registry.NewCache() + dictListConfig := map[string]interface{}{ + "type": tokenmap.Name, + "tokens": []interface{}{"soft", "softest", "ball"}, + } + _, err := cache.DefineTokenMap("dict_test", dictListConfig) + if err != nil { + t.Fatal(err) + } + + dictConfig := map[string]interface{}{ + "type": "dict_compound", + "dict_token_map": "dict_test", + "only_longest_match": true, + } + dictFilter, err := cache.DefineTokenFilter("dict_test", dictConfig) + if err != nil { + t.Fatal(err) + } + + ouputTokenStream := dictFilter.Filter(inputTokenStream) + if !reflect.DeepEqual(ouputTokenStream, expectedTokenStream) { + t.Errorf("expected %#v got %#v", expectedTokenStream, ouputTokenStream) + } +} diff --git a/analysis/token/edgengram/edgengram.go b/analysis/token/edgengram/edgengram.go new file mode 100644 index 0000000..d9e4c24 --- /dev/null +++ b/analysis/token/edgengram/edgengram.go @@ -0,0 +1,118 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package edgengram + +import ( + "bytes" + "fmt" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "edge_ngram" + +type Side bool + +const BACK Side = true +const FRONT Side = false + +type EdgeNgramFilter struct { + back Side + minLength int + maxLength int +} + +func NewEdgeNgramFilter(side Side, minLength, maxLength int) *EdgeNgramFilter { + return &EdgeNgramFilter{ + back: side, + minLength: minLength, + maxLength: maxLength, + } +} + +func (s *EdgeNgramFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + rv := make(analysis.TokenStream, 0, len(input)) + + for _, token := range input { + runeCount := utf8.RuneCount(token.Term) + runes := bytes.Runes(token.Term) + if s.back { + i := runeCount + // index of the starting rune for this token + for ngramSize := s.minLength; ngramSize <= s.maxLength; ngramSize++ { + // build an ngram of this size starting at i + if i-ngramSize >= 0 { + ngramTerm := analysis.BuildTermFromRunes(runes[i-ngramSize : i]) + token := analysis.Token{ + Position: token.Position, + Start: token.Start, + End: token.End, + Type: token.Type, + Term: ngramTerm, + } + rv = append(rv, &token) + } + } + } else { + i := 0 + // index of the starting rune for this token + for ngramSize := s.minLength; ngramSize <= s.maxLength; ngramSize++ { + // build an ngram of this size starting at i + if i+ngramSize <= runeCount { + ngramTerm := analysis.BuildTermFromRunes(runes[i : i+ngramSize]) + token := analysis.Token{ + Position: token.Position, + Start: token.Start, + End: token.End, + Type: token.Type, + Term: ngramTerm, + } + rv = append(rv, &token) + } + } + } + } + + return rv +} + +func EdgeNgramFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + side := FRONT + back, ok := config["back"].(bool) + if ok && back { + side = BACK + } + minVal, ok := config["min"].(float64) + if !ok { + return nil, fmt.Errorf("must specify min") + } + min := int(minVal) + maxVal, ok := config["max"].(float64) + if !ok { + return nil, fmt.Errorf("must specify max") + } + max := int(maxVal) + + return NewEdgeNgramFilter(side, min, max), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, EdgeNgramFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/edgengram/edgengram_test.go b/analysis/token/edgengram/edgengram_test.go new file mode 100644 index 0000000..cb206a5 --- /dev/null +++ b/analysis/token/edgengram/edgengram_test.go @@ -0,0 +1,189 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package edgengram + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestEdgeNgramFilter(t *testing.T) { + + tests := []struct { + side Side + min int + max int + input analysis.TokenStream + output analysis.TokenStream + }{ + { + side: FRONT, + min: 1, + max: 1, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + }, + }, + { + side: BACK, + min: 1, + max: 1, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("e"), + }, + }, + }, + { + side: FRONT, + min: 1, + max: 3, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("ab"), + }, + &analysis.Token{ + Term: []byte("abc"), + }, + }, + }, + { + side: BACK, + min: 1, + max: 3, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("e"), + }, + &analysis.Token{ + Term: []byte("de"), + }, + &analysis.Token{ + Term: []byte("cde"), + }, + }, + }, + { + side: FRONT, + min: 1, + max: 3, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + &analysis.Token{ + Term: []byte("vwxyz"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("ab"), + }, + &analysis.Token{ + Term: []byte("abc"), + }, + &analysis.Token{ + Term: []byte("v"), + }, + &analysis.Token{ + Term: []byte("vw"), + }, + &analysis.Token{ + Term: []byte("vwx"), + }, + }, + }, + { + side: BACK, + min: 3, + max: 5, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Beryl"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ryl"), + }, + &analysis.Token{ + Term: []byte("eryl"), + }, + &analysis.Token{ + Term: []byte("Beryl"), + }, + }, + }, + { + side: FRONT, + min: 3, + max: 5, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Beryl"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Ber"), + }, + &analysis.Token{ + Term: []byte("Bery"), + }, + &analysis.Token{ + Term: []byte("Beryl"), + }, + }, + }, + } + + for _, test := range tests { + edgeNgramFilter := NewEdgeNgramFilter(test.side, test.min, test.max) + actual := edgeNgramFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + } +} diff --git a/analysis/token/elision/elision.go b/analysis/token/elision/elision.go new file mode 100644 index 0000000..721a546 --- /dev/null +++ b/analysis/token/elision/elision.go @@ -0,0 +1,77 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package elision + +import ( + "fmt" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "elision" + +const RightSingleQuotationMark = '’' +const Apostrophe = '\'' + +type ElisionFilter struct { + articles analysis.TokenMap +} + +func NewElisionFilter(articles analysis.TokenMap) *ElisionFilter { + return &ElisionFilter{ + articles: articles, + } +} + +func (s *ElisionFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + term := token.Term + for i := 0; i < len(term); { + r, size := utf8.DecodeRune(term[i:]) + if r == Apostrophe || r == RightSingleQuotationMark { + // see if the prefix matches one of the articles + prefix := term[0:i] + _, articleMatch := s.articles[string(prefix)] + if articleMatch { + token.Term = term[i+size:] + break + } + } + i += size + } + } + return input +} + +func ElisionFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + articlesTokenMapName, ok := config["articles_token_map"].(string) + if !ok { + return nil, fmt.Errorf("must specify articles_token_map") + } + articlesTokenMap, err := cache.TokenMapNamed(articlesTokenMapName) + if err != nil { + return nil, fmt.Errorf("error building elision filter: %v", err) + } + return NewElisionFilter(articlesTokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, ElisionFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/elision/elision_test.go b/analysis/token/elision/elision_test.go new file mode 100644 index 0000000..ed580b0 --- /dev/null +++ b/analysis/token/elision/elision_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package elision + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenmap" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestElisionFilter(t *testing.T) { + + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ar" + string(Apostrophe) + "word"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("word"), + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ar" + string(RightSingleQuotationMark) + "word"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("word"), + }, + }, + }, + } + + cache := registry.NewCache() + + articleListConfig := map[string]interface{}{ + "type": tokenmap.Name, + "tokens": []interface{}{"ar"}, + } + _, err := cache.DefineTokenMap("articles_test", articleListConfig) + if err != nil { + t.Fatal(err) + } + + elisionConfig := map[string]interface{}{ + "type": "elision", + "articles_token_map": "articles_test", + } + elisionFilter, err := cache.DefineTokenFilter("elision_test", elisionConfig) + if err != nil { + t.Fatal(err) + } + + for _, test := range tests { + + actual := elisionFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/token/hierarchy/hierarchy.go b/analysis/token/hierarchy/hierarchy.go new file mode 100644 index 0000000..6e1ea44 --- /dev/null +++ b/analysis/token/hierarchy/hierarchy.go @@ -0,0 +1,95 @@ +package hierarchy + +import ( + "bytes" + "fmt" + "math" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "hierarchy" + +type HierarchyFilter struct { + maxLevels int + delimiter []byte + splitInput bool +} + +func NewHierarchyFilter(delimiter []byte, maxLevels int, splitInput bool) *HierarchyFilter { + return &HierarchyFilter{ + maxLevels: maxLevels, + delimiter: delimiter, + splitInput: splitInput, + } +} + +func (s *HierarchyFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + rv := make(analysis.TokenStream, 0, s.maxLevels) + + var soFar [][]byte + for _, token := range input { + if s.splitInput { + parts := bytes.Split(token.Term, s.delimiter) + for _, part := range parts { + soFar, rv = s.buildToken(rv, soFar, part) + if len(soFar) >= s.maxLevels { + return rv + } + } + } else { + soFar, rv = s.buildToken(rv, soFar, token.Term) + if len(soFar) >= s.maxLevels { + return rv + } + } + } + + return rv +} + +func (s *HierarchyFilter) buildToken(tokenStream analysis.TokenStream, soFar [][]byte, part []byte) ( + [][]byte, analysis.TokenStream) { + + soFar = append(soFar, part) + term := bytes.Join(soFar, s.delimiter) + + tokenStream = append(tokenStream, &analysis.Token{ + Type: analysis.Shingle, + Term: term, + Start: 0, + End: len(term), + Position: 1, + }) + + return soFar, tokenStream +} + +func HierarchyFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + max := math.MaxInt64 + maxVal, ok := config["max"].(float64) + if ok { + max = int(maxVal) + } + + splitInput := true + splitInputVal, ok := config["split_input"].(bool) + if ok { + splitInput = splitInputVal + } + + delimiter, ok := config["delimiter"].(string) + if !ok { + return nil, fmt.Errorf("must specify delimiter") + } + + return NewHierarchyFilter([]byte(delimiter), max, splitInput), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, HierarchyFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/hierarchy/hierarchy_test.go b/analysis/token/hierarchy/hierarchy_test.go new file mode 100644 index 0000000..74d24bf --- /dev/null +++ b/analysis/token/hierarchy/hierarchy_test.go @@ -0,0 +1,229 @@ +package hierarchy + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestHierarchyFilter(t *testing.T) { + + tests := []struct { + name string + delimiter string + max int + splitInput bool + + input analysis.TokenStream + output analysis.TokenStream + }{ + { + name: "single token a/b/c, delimiter /", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a/b/c"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + Type: analysis.Shingle, + Start: 0, + End: 1, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b"), + Type: analysis.Shingle, + Start: 0, + End: 3, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b/c"), + Type: analysis.Shingle, + Start: 0, + End: 5, + Position: 1, + }, + }, + delimiter: "/", + max: 10, + splitInput: true, + }, + { + name: "multiple tokens already split a b c, delimiter /", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("b"), + }, + &analysis.Token{ + Term: []byte("c"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + Type: analysis.Shingle, + Start: 0, + End: 1, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b"), + Type: analysis.Shingle, + Start: 0, + End: 3, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b/c"), + Type: analysis.Shingle, + Start: 0, + End: 5, + Position: 1, + }, + }, + delimiter: "/", + max: 10, + splitInput: true, + }, + { + name: "single token a/b/c, delimiter /, limit 2", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a/b/c"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + Type: analysis.Shingle, + Start: 0, + End: 1, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b"), + Type: analysis.Shingle, + Start: 0, + End: 3, + Position: 1, + }, + }, + delimiter: "/", + max: 2, + splitInput: true, + }, + { + name: "multiple tokens already split a b c, delimiter /, limit 2", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("b"), + }, + &analysis.Token{ + Term: []byte("c"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + Type: analysis.Shingle, + Start: 0, + End: 1, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b"), + Type: analysis.Shingle, + Start: 0, + End: 3, + Position: 1, + }, + }, + delimiter: "/", + max: 2, + splitInput: true, + }, + + { + name: "single token a/b/c, delimiter /, no split", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a/b/c"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a/b/c"), + Type: analysis.Shingle, + Start: 0, + End: 5, + Position: 1, + }, + }, + delimiter: "/", + max: 10, + splitInput: false, + }, + { + name: "multiple tokens already split a b c, delimiter /, no split", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("b"), + }, + &analysis.Token{ + Term: []byte("c"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + Type: analysis.Shingle, + Start: 0, + End: 1, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b"), + Type: analysis.Shingle, + Start: 0, + End: 3, + Position: 1, + }, + &analysis.Token{ + Term: []byte("a/b/c"), + Type: analysis.Shingle, + Start: 0, + End: 5, + Position: 1, + }, + }, + delimiter: "/", + max: 10, + splitInput: false, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + filter := NewHierarchyFilter([]byte(test.delimiter), test.max, test.splitInput) + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + }) + } + +} diff --git a/analysis/token/keyword/keyword.go b/analysis/token/keyword/keyword.go new file mode 100644 index 0000000..f2e56f3 --- /dev/null +++ b/analysis/token/keyword/keyword.go @@ -0,0 +1,63 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package keyword + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "keyword_marker" + +type KeyWordMarkerFilter struct { + keyWords analysis.TokenMap +} + +func NewKeyWordMarkerFilter(keyWords analysis.TokenMap) *KeyWordMarkerFilter { + return &KeyWordMarkerFilter{ + keyWords: keyWords, + } +} + +func (f *KeyWordMarkerFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + _, isKeyWord := f.keyWords[string(token.Term)] + if isKeyWord { + token.KeyWord = true + } + } + return input +} + +func KeyWordMarkerFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + keywordsTokenMapName, ok := config["keywords_token_map"].(string) + if !ok { + return nil, fmt.Errorf("must specify keywords_token_map") + } + keywordsTokenMap, err := cache.TokenMapNamed(keywordsTokenMapName) + if err != nil { + return nil, fmt.Errorf("error building keyword marker filter: %v", err) + } + return NewKeyWordMarkerFilter(keywordsTokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, KeyWordMarkerFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/keyword/keyword_test.go b/analysis/token/keyword/keyword_test.go new file mode 100644 index 0000000..8b789f1 --- /dev/null +++ b/analysis/token/keyword/keyword_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package keyword + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestKeyWordMarkerFilter(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("walk"), + }, + &analysis.Token{ + Term: []byte("in"), + }, + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("park"), + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("walk"), + KeyWord: true, + }, + &analysis.Token{ + Term: []byte("in"), + }, + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("park"), + KeyWord: true, + }, + } + + keyWordsMap := analysis.NewTokenMap() + keyWordsMap.AddToken("walk") + keyWordsMap.AddToken("park") + + filter := NewKeyWordMarkerFilter(keyWordsMap) + ouputTokenStream := filter.Filter(inputTokenStream) + if !reflect.DeepEqual(ouputTokenStream, expectedTokenStream) { + t.Errorf("expected %#v got %#v", expectedTokenStream[0].KeyWord, ouputTokenStream[0].KeyWord) + } +} diff --git a/analysis/token/length/length.go b/analysis/token/length/length.go new file mode 100644 index 0000000..1091270 --- /dev/null +++ b/analysis/token/length/length.go @@ -0,0 +1,80 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package length + +import ( + "fmt" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "length" + +type LengthFilter struct { + min int + max int +} + +func NewLengthFilter(min, max int) *LengthFilter { + return &LengthFilter{ + min: min, + max: max, + } +} + +func (f *LengthFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + rv := make(analysis.TokenStream, 0, len(input)) + + for _, token := range input { + wordLen := utf8.RuneCount(token.Term) + if f.min > 0 && f.min > wordLen { + continue + } + if f.max > 0 && f.max < wordLen { + continue + } + rv = append(rv, token) + } + + return rv +} + +func LengthFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + min := 0 + max := 0 + + minVal, ok := config["min"].(float64) + if ok { + min = int(minVal) + } + maxVal, ok := config["max"].(float64) + if ok { + max = int(maxVal) + } + if min == max && max == 0 { + return nil, fmt.Errorf("either min or max must be non-zero") + } + + return NewLengthFilter(min, max), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, LengthFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/length/length_test.go b/analysis/token/length/length_test.go new file mode 100644 index 0000000..b3cb064 --- /dev/null +++ b/analysis/token/length/length_test.go @@ -0,0 +1,99 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package length + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestLengthFilter(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1"), + }, + &analysis.Token{ + Term: []byte("two"), + }, + &analysis.Token{ + Term: []byte("three"), + }, + } + + lengthFilter := NewLengthFilter(3, 4) + ouputTokenStream := lengthFilter.Filter(inputTokenStream) + if len(ouputTokenStream) != 1 { + t.Fatalf("expected 1 output token") + } + if string(ouputTokenStream[0].Term) != "two" { + t.Errorf("expected term `two`, got `%s`", ouputTokenStream[0].Term) + } +} + +func TestLengthFilterNoMax(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1"), + }, + &analysis.Token{ + Term: []byte("two"), + }, + &analysis.Token{ + Term: []byte("three"), + }, + } + + lengthFilter := NewLengthFilter(3, -1) + ouputTokenStream := lengthFilter.Filter(inputTokenStream) + if len(ouputTokenStream) != 2 { + t.Fatalf("expected 2 output token") + } + if string(ouputTokenStream[0].Term) != "two" { + t.Errorf("expected term `two`, got `%s`", ouputTokenStream[0].Term) + } + if string(ouputTokenStream[1].Term) != "three" { + t.Errorf("expected term `three`, got `%s`", ouputTokenStream[0].Term) + } +} + +func TestLengthFilterNoMin(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1"), + }, + &analysis.Token{ + Term: []byte("two"), + }, + &analysis.Token{ + Term: []byte("three"), + }, + } + + lengthFilter := NewLengthFilter(-1, 4) + ouputTokenStream := lengthFilter.Filter(inputTokenStream) + if len(ouputTokenStream) != 2 { + t.Fatalf("expected 2 output token") + } + if string(ouputTokenStream[0].Term) != "1" { + t.Errorf("expected term `1`, got `%s`", ouputTokenStream[0].Term) + } + if string(ouputTokenStream[1].Term) != "two" { + t.Errorf("expected term `two`, got `%s`", ouputTokenStream[0].Term) + } +} diff --git a/analysis/token/lowercase/lowercase.go b/analysis/token/lowercase/lowercase.go new file mode 100644 index 0000000..92b9628 --- /dev/null +++ b/analysis/token/lowercase/lowercase.go @@ -0,0 +1,108 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package lowercase implements a TokenFilter which converts +// tokens to lower case according to unicode rules. +package lowercase + +import ( + "bytes" + "unicode" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +// Name is the name used to register LowerCaseFilter in the bleve registry +const Name = "to_lower" + +type LowerCaseFilter struct { +} + +func NewLowerCaseFilter() *LowerCaseFilter { + return &LowerCaseFilter{} +} + +func (f *LowerCaseFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + token.Term = toLowerDeferredCopy(token.Term) + } + return input +} + +func LowerCaseFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewLowerCaseFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, LowerCaseFilterConstructor) + if err != nil { + panic(err) + } +} + +// toLowerDeferredCopy will function exactly like +// bytes.ToLower() only it will reuse (overwrite) +// the original byte array when possible +// NOTE: because its possible that the lower-case +// form of a rune has a different utf-8 encoded +// length, in these cases a new byte array is allocated +func toLowerDeferredCopy(s []byte) []byte { + j := 0 + for i := 0; i < len(s); { + wid := 1 + r := rune(s[i]) + if r >= utf8.RuneSelf { + r, wid = utf8.DecodeRune(s[i:]) + } + + l := unicode.ToLower(r) + + // If the rune is already lowercased, just move to the + // next rune. + if l == r { + i += wid + j += wid + continue + } + + // Handles the Unicode edge-case where the last + // rune in a word on the greek Σ needs to be converted + // differently. + if l == 'σ' && i+2 == len(s) { + l = 'ς' + } + + lwid := utf8.RuneLen(l) + if lwid > wid { + // utf-8 encoded replacement is wider + // for now, punt and defer + // to bytes.ToLower() for the remainder + // only known to happen with chars + // Rune Ⱥ(570) width 2 - Lower ⱥ(11365) width 3 + // Rune Ⱦ(574) width 2 - Lower ⱦ(11366) width 3 + rest := bytes.ToLower(s[i:]) + rv := make([]byte, j+len(rest)) + copy(rv[:j], s[:j]) + copy(rv[j:], rest) + return rv + } else { + utf8.EncodeRune(s[j:], l) + } + i += wid + j += lwid + } + return s[:j] +} diff --git a/analysis/token/lowercase/lowercase_test.go b/analysis/token/lowercase/lowercase_test.go new file mode 100644 index 0000000..95fd2af --- /dev/null +++ b/analysis/token/lowercase/lowercase_test.go @@ -0,0 +1,166 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lowercase + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestLowerCaseFilter(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ONE"), + }, + &analysis.Token{ + Term: []byte("two"), + }, + &analysis.Token{ + Term: []byte("ThReE"), + }, + &analysis.Token{ + Term: []byte("steven's"), + }, + // these characters are chosen in particular + // because the utf-8 encoding of the lower-case + // version has a different length + // Rune İ(304) width 2 - Lower i(105) width 1 + // Rune Ⱥ(570) width 2 - Lower ⱥ(11365) width 3 + // Rune Ⱦ(574) width 2 - Lower ⱦ(11366) width 3 + &analysis.Token{ + Term: []byte("İȺȾCAT"), + }, + &analysis.Token{ + Term: []byte("ȺȾCAT"), + }, + &analysis.Token{ + Term: []byte("ὈΔΥΣΣ"), + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("one"), + }, + &analysis.Token{ + Term: []byte("two"), + }, + &analysis.Token{ + Term: []byte("three"), + }, + &analysis.Token{ + Term: []byte("steven's"), + }, + &analysis.Token{ + Term: []byte("iⱥⱦcat"), + }, + &analysis.Token{ + Term: []byte("ⱥⱦcat"), + }, + &analysis.Token{ + Term: []byte("ὀδυσς"), + }, + } + + filter := NewLowerCaseFilter() + ouputTokenStream := filter.Filter(inputTokenStream) + if !reflect.DeepEqual(ouputTokenStream, expectedTokenStream) { + t.Errorf("expected %#v got %#v", expectedTokenStream, ouputTokenStream) + t.Errorf("expected %s got %s", expectedTokenStream[0].Term, ouputTokenStream[0].Term) + } +} + +func BenchmarkLowerCaseFilter(b *testing.B) { + input := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("A"), + }, + &analysis.Token{ + Term: []byte("boiling"), + }, + &analysis.Token{ + Term: []byte("liquid"), + }, + &analysis.Token{ + Term: []byte("expanding"), + }, + &analysis.Token{ + Term: []byte("vapor"), + }, + &analysis.Token{ + Term: []byte("explosion"), + }, + &analysis.Token{ + Term: []byte("caused"), + }, + &analysis.Token{ + Term: []byte("by"), + }, + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("rupture"), + }, + &analysis.Token{ + Term: []byte("of"), + }, + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("vessel"), + }, + &analysis.Token{ + Term: []byte("containing"), + }, + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("pressurized"), + }, + &analysis.Token{ + Term: []byte("liquid"), + }, + &analysis.Token{ + Term: []byte("above"), + }, + &analysis.Token{ + Term: []byte("its"), + }, + &analysis.Token{ + Term: []byte("boiling"), + }, + &analysis.Token{ + Term: []byte("point"), + }, + &analysis.Token{ + Term: []byte("İȺȾCAT"), + }, + &analysis.Token{ + Term: []byte("ȺȾCAT"), + }, + } + filter := NewLowerCaseFilter() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + filter.Filter(input) + } +} diff --git a/analysis/token/ngram/ngram.go b/analysis/token/ngram/ngram.go new file mode 100644 index 0000000..d8967a0 --- /dev/null +++ b/analysis/token/ngram/ngram.go @@ -0,0 +1,116 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ngram + +import ( + "bytes" + "fmt" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "ngram" + +type NgramFilter struct { + minLength int + maxLength int +} + +func NewNgramFilter(minLength, maxLength int) *NgramFilter { + return &NgramFilter{ + minLength: minLength, + maxLength: maxLength, + } +} + +func (s *NgramFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + rv := make(analysis.TokenStream, 0, len(input)) + + for _, token := range input { + runeCount := utf8.RuneCount(token.Term) + runes := bytes.Runes(token.Term) + for i := 0; i < runeCount; i++ { + // index of the starting rune for this token + for ngramSize := s.minLength; ngramSize <= s.maxLength; ngramSize++ { + // build an ngram of this size starting at i + if i+ngramSize <= runeCount { + ngramTerm := analysis.BuildTermFromRunes(runes[i : i+ngramSize]) + token := analysis.Token{ + Position: token.Position, + Start: token.Start, + End: token.End, + Type: token.Type, + Term: ngramTerm, + } + rv = append(rv, &token) + } + } + } + } + + return rv +} + +func NgramFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + minVal, ok := config["min"] + if !ok { + return nil, fmt.Errorf("must specify min") + } + + min, err := convertToInt(minVal) + if err != nil { + return nil, err + } + + maxVal, ok := config["max"] + if !ok { + return nil, fmt.Errorf("must specify max") + } + + max, err := convertToInt(maxVal) + if err != nil { + return nil, err + } + + return NewNgramFilter(min, max), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, NgramFilterConstructor) + if err != nil { + panic(err) + } +} + +// Expects either an int or a flaot64 value +func convertToInt(val interface{}) (int, error) { + var intVal int + var floatVal float64 + var ok bool + + intVal, ok = val.(int) + if ok { + return intVal, nil + } + + floatVal, ok = val.(float64) + if ok { + return int(floatVal), nil + } + + return 0, fmt.Errorf("failed to convert to int value") +} diff --git a/analysis/token/ngram/ngram_test.go b/analysis/token/ngram/ngram_test.go new file mode 100644 index 0000000..877c881 --- /dev/null +++ b/analysis/token/ngram/ngram_test.go @@ -0,0 +1,192 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ngram + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestNgramFilter(t *testing.T) { + + tests := []struct { + min int + max int + input analysis.TokenStream + output analysis.TokenStream + }{ + { + min: 1, + max: 1, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("b"), + }, + &analysis.Token{ + Term: []byte("c"), + }, + &analysis.Token{ + Term: []byte("d"), + }, + &analysis.Token{ + Term: []byte("e"), + }, + }, + }, + { + min: 2, + max: 2, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ab"), + }, + &analysis.Token{ + Term: []byte("bc"), + }, + &analysis.Token{ + Term: []byte("cd"), + }, + &analysis.Token{ + Term: []byte("de"), + }, + }, + }, + { + min: 1, + max: 3, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("ab"), + }, + &analysis.Token{ + Term: []byte("abc"), + }, + &analysis.Token{ + Term: []byte("b"), + }, + &analysis.Token{ + Term: []byte("bc"), + }, + &analysis.Token{ + Term: []byte("bcd"), + }, + &analysis.Token{ + Term: []byte("c"), + }, + &analysis.Token{ + Term: []byte("cd"), + }, + &analysis.Token{ + Term: []byte("cde"), + }, + &analysis.Token{ + Term: []byte("d"), + }, + &analysis.Token{ + Term: []byte("de"), + }, + &analysis.Token{ + Term: []byte("e"), + }, + }, + }, + } + + for _, test := range tests { + ngramFilter := NewNgramFilter(test.min, test.max) + actual := ngramFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + } +} + +func TestConversionInt(t *testing.T) { + config := map[string]interface{}{ + "type": Name, + "min": 3, + "max": 8, + } + + f, err := NgramFilterConstructor(config, nil) + + if err != nil { + t.Errorf("Failed to construct the ngram filter: %v", err) + } + + ngram := f.(*NgramFilter) + if ngram.minLength != 3 && ngram.maxLength != 8 { + t.Errorf("Failed to construct the bounds. Got %v and %v.", ngram.minLength, ngram.maxLength) + } +} + +func TestConversionFloat(t *testing.T) { + config := map[string]interface{}{ + "type": Name, + "min": float64(3), + "max": float64(8), + } + + f, err := NgramFilterConstructor(config, nil) + + if err != nil { + t.Errorf("Failed to construct the ngram filter: %v", err) + } + + ngram := f.(*NgramFilter) + if ngram.minLength != 3 && ngram.maxLength != 8 { + t.Errorf("Failed to construct the bounds. Got %v and %v.", ngram.minLength, ngram.maxLength) + } +} + +func TestBadConversion(t *testing.T) { + config := map[string]interface{}{ + "type": Name, + "min": "3", + } + + _, err := NgramFilterConstructor(config, nil) + + if err == nil { + t.Errorf("Expected conversion error.") + } + + if err.Error() != "failed to convert to int value" { + t.Errorf("Wrong error recevied. Got %v.", err) + } +} diff --git a/analysis/token/porter/porter.go b/analysis/token/porter/porter.go new file mode 100644 index 0000000..ed1574b --- /dev/null +++ b/analysis/token/porter/porter.go @@ -0,0 +1,56 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package porter + +import ( + "bytes" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/go-porterstemmer" +) + +const Name = "stemmer_porter" + +type PorterStemmer struct { +} + +func NewPorterStemmer() *PorterStemmer { + return &PorterStemmer{} +} + +func (s *PorterStemmer) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + // if it is not a protected keyword, stem it + if !token.KeyWord { + termRunes := bytes.Runes(token.Term) + stemmedRunes := porterstemmer.StemWithoutLowerCasing(termRunes) + token.Term = analysis.BuildTermFromRunes(stemmedRunes) + } + } + return input +} + +func PorterStemmerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewPorterStemmer(), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, PorterStemmerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/porter/porter_test.go b/analysis/token/porter/porter_test.go new file mode 100644 index 0000000..3e0a79f --- /dev/null +++ b/analysis/token/porter/porter_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package porter + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestPorterStemmer(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("walking"), + }, + &analysis.Token{ + Term: []byte("talked"), + }, + &analysis.Token{ + Term: []byte("business"), + }, + &analysis.Token{ + Term: []byte("protected"), + KeyWord: true, + }, + &analysis.Token{ + Term: []byte("cat"), + }, + &analysis.Token{ + Term: []byte("done"), + }, + // a term which does stem, but does not change length + &analysis.Token{ + Term: []byte("marty"), + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("walk"), + }, + &analysis.Token{ + Term: []byte("talk"), + }, + &analysis.Token{ + Term: []byte("busi"), + }, + &analysis.Token{ + Term: []byte("protected"), + KeyWord: true, + }, + &analysis.Token{ + Term: []byte("cat"), + }, + &analysis.Token{ + Term: []byte("done"), + }, + &analysis.Token{ + Term: []byte("marti"), + }, + } + + filter := NewPorterStemmer() + ouputTokenStream := filter.Filter(inputTokenStream) + if !reflect.DeepEqual(ouputTokenStream, expectedTokenStream) { + t.Errorf("expected %#v got %#v", expectedTokenStream[3], ouputTokenStream[3]) + } +} + +func BenchmarkPorterStemmer(b *testing.B) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("walking"), + }, + &analysis.Token{ + Term: []byte("talked"), + }, + &analysis.Token{ + Term: []byte("business"), + }, + &analysis.Token{ + Term: []byte("protected"), + KeyWord: true, + }, + &analysis.Token{ + Term: []byte("cat"), + }, + &analysis.Token{ + Term: []byte("done"), + }, + } + + filter := NewPorterStemmer() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + filter.Filter(inputTokenStream) + } + +} diff --git a/analysis/token/reverse/reverse.go b/analysis/token/reverse/reverse.go new file mode 100644 index 0000000..5eb708f --- /dev/null +++ b/analysis/token/reverse/reverse.go @@ -0,0 +1,78 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reverse + +import ( + "unicode" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +// Name is the name used to register ReverseFilter in the bleve registry +const Name = "reverse" + +type ReverseFilter struct { +} + +func NewReverseFilter() *ReverseFilter { + return &ReverseFilter{} +} + +func (f *ReverseFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + token.Term = reverse(token.Term) + } + return input +} + +func ReverseFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewReverseFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, ReverseFilterConstructor) + if err != nil { + panic(err) + } +} + +// reverse(..) will generate a reversed version of the provided +// unicode array and return it back to its caller. +func reverse(s []byte) []byte { + cursorIn := 0 + inputRunes := []rune(string(s)) + cursorOut := len(s) + output := make([]byte, len(s)) + for i := 0; i < len(inputRunes); { + wid := utf8.RuneLen(inputRunes[i]) + i++ + for i < len(inputRunes) { + r := inputRunes[i] + if unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Me, r) || unicode.Is(unicode.Mc, r) { + wid += utf8.RuneLen(r) + i++ + } else { + break + } + } + copy(output[cursorOut-wid:cursorOut], s[cursorIn:cursorIn+wid]) + cursorIn += wid + cursorOut -= wid + } + + return output +} diff --git a/analysis/token/reverse/reverse_test.go b/analysis/token/reverse/reverse_test.go new file mode 100644 index 0000000..4bb7626 --- /dev/null +++ b/analysis/token/reverse/reverse_test.go @@ -0,0 +1,184 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reverse + +import ( + "bytes" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestReverseFilter(t *testing.T) { + inputTokenStream := analysis.TokenStream{ + &analysis.Token{}, + &analysis.Token{ + Term: []byte("one"), + }, + &analysis.Token{ + Term: []byte("TWo"), + }, + &analysis.Token{ + Term: []byte("thRee"), + }, + &analysis.Token{ + Term: []byte("four's"), + }, + &analysis.Token{ + Term: []byte("what's this in reverse"), + }, + &analysis.Token{ + Term: []byte("œ∑´®†"), + }, + &analysis.Token{ + Term: []byte("İȺȾCAT÷≥≤µ123"), + }, + &analysis.Token{ + Term: []byte("!@#$%^&*()"), + }, + &analysis.Token{ + Term: []byte("cafés"), + }, + &analysis.Token{ + Term: []byte("¿Dónde estás?"), + }, + &analysis.Token{ + Term: []byte("Me gustaría una cerveza."), + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{}, + &analysis.Token{ + Term: []byte("eno"), + }, + &analysis.Token{ + Term: []byte("oWT"), + }, + &analysis.Token{ + Term: []byte("eeRht"), + }, + &analysis.Token{ + Term: []byte("s'ruof"), + }, + &analysis.Token{ + Term: []byte("esrever ni siht s'tahw"), + }, + &analysis.Token{ + Term: []byte("†®´∑œ"), + }, + &analysis.Token{ + Term: []byte("321µ≤≥÷TACȾȺİ"), + }, + &analysis.Token{ + Term: []byte(")(*&^%$#@!"), + }, + &analysis.Token{ + Term: []byte("séfac"), + }, + &analysis.Token{ + Term: []byte("?sátse ednóD¿"), + }, + &analysis.Token{ + Term: []byte(".azevrec anu aíratsug eM"), + }, + } + + filter := NewReverseFilter() + outputTokenStream := filter.Filter(inputTokenStream) + for i := 0; i < len(expectedTokenStream); i++ { + if !bytes.Equal(outputTokenStream[i].Term, expectedTokenStream[i].Term) { + t.Errorf("[%d] expected %s got %s", + i+1, expectedTokenStream[i].Term, outputTokenStream[i].Term) + } + } +} + +func BenchmarkReverseFilter(b *testing.B) { + input := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("A"), + }, + &analysis.Token{ + Term: []byte("boiling"), + }, + &analysis.Token{ + Term: []byte("liquid"), + }, + &analysis.Token{ + Term: []byte("expanding"), + }, + &analysis.Token{ + Term: []byte("vapor"), + }, + &analysis.Token{ + Term: []byte("explosion"), + }, + &analysis.Token{ + Term: []byte("caused"), + }, + &analysis.Token{ + Term: []byte("by"), + }, + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("rupture"), + }, + &analysis.Token{ + Term: []byte("of"), + }, + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("vessel"), + }, + &analysis.Token{ + Term: []byte("containing"), + }, + &analysis.Token{ + Term: []byte("pressurized"), + }, + &analysis.Token{ + Term: []byte("liquid"), + }, + &analysis.Token{ + Term: []byte("above"), + }, + &analysis.Token{ + Term: []byte("its"), + }, + &analysis.Token{ + Term: []byte("boiling"), + }, + &analysis.Token{ + Term: []byte("point"), + }, + &analysis.Token{ + Term: []byte("İȺȾCAT"), + }, + &analysis.Token{ + Term: []byte("Me gustaría una cerveza."), + }, + } + filter := NewReverseFilter() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + filter.Filter(input) + } +} diff --git a/analysis/token/shingle/shingle.go b/analysis/token/shingle/shingle.go new file mode 100644 index 0000000..29280a7 --- /dev/null +++ b/analysis/token/shingle/shingle.go @@ -0,0 +1,172 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shingle + +import ( + "container/ring" + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "shingle" + +type ShingleFilter struct { + min int + max int + outputOriginal bool + tokenSeparator string + fill string +} + +func NewShingleFilter(min, max int, outputOriginal bool, sep, fill string) *ShingleFilter { + return &ShingleFilter{ + min: min, + max: max, + outputOriginal: outputOriginal, + tokenSeparator: sep, + fill: fill, + } +} + +func (s *ShingleFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + rv := make(analysis.TokenStream, 0, len(input)) + + ring := ring.New(s.max) + itemsInRing := 0 + currentPosition := 0 + for _, token := range input { + if s.outputOriginal { + rv = append(rv, token) + } + + // if there are gaps, insert filler tokens + offset := token.Position - currentPosition + for offset > 1 { + fillerToken := analysis.Token{ + Position: 0, + Start: -1, + End: -1, + Type: analysis.AlphaNumeric, + Term: []byte(s.fill), + } + ring.Value = &fillerToken + if itemsInRing < s.max { + itemsInRing++ + } + rv = append(rv, s.shingleCurrentRingState(ring, itemsInRing)...) + ring = ring.Next() + offset-- + } + currentPosition = token.Position + + ring.Value = token + if itemsInRing < s.max { + itemsInRing++ + } + rv = append(rv, s.shingleCurrentRingState(ring, itemsInRing)...) + ring = ring.Next() + } + + return rv +} + +func (s *ShingleFilter) shingleCurrentRingState(ring *ring.Ring, itemsInRing int) analysis.TokenStream { + rv := make(analysis.TokenStream, 0) + for shingleN := s.min; shingleN <= s.max; shingleN++ { + // if there are enough items in the ring + // to produce a shingle of this size + if itemsInRing >= shingleN { + thisShingleRing := ring.Move(-(shingleN - 1)) + shingledBytes := make([]byte, 0) + pos := 0 + start := -1 + end := 0 + for i := 0; i < shingleN; i++ { + if i != 0 { + shingledBytes = append(shingledBytes, []byte(s.tokenSeparator)...) + } + curr := thisShingleRing.Value.(*analysis.Token) + if pos == 0 && curr.Position != 0 { + pos = curr.Position + } + if start == -1 && curr.Start != -1 { + start = curr.Start + } + if curr.End != -1 { + end = curr.End + } + shingledBytes = append(shingledBytes, curr.Term...) + thisShingleRing = thisShingleRing.Next() + } + token := analysis.Token{ + Type: analysis.Shingle, + Term: shingledBytes, + } + if pos != 0 { + token.Position = pos + } + if start != -1 { + token.Start = start + } + if end != -1 { + token.End = end + } + rv = append(rv, &token) + } + } + return rv +} + +func ShingleFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + minVal, ok := config["min"].(float64) + if !ok { + return nil, fmt.Errorf("must specify min") + } + min := int(minVal) + maxVal, ok := config["max"].(float64) + if !ok { + return nil, fmt.Errorf("must specify max") + } + max := int(maxVal) + + outputOriginal := false + outVal, ok := config["output_original"].(bool) + if ok { + outputOriginal = outVal + } + + sep := " " + sepVal, ok := config["separator"].(string) + if ok { + sep = sepVal + } + + fill := "_" + fillVal, ok := config["filler"].(string) + if ok { + fill = fillVal + } + + return NewShingleFilter(min, max, outputOriginal, sep, fill), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, ShingleFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/shingle/shingle_test.go b/analysis/token/shingle/shingle_test.go new file mode 100644 index 0000000..b545056 --- /dev/null +++ b/analysis/token/shingle/shingle_test.go @@ -0,0 +1,416 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shingle + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestShingleFilter(t *testing.T) { + + tests := []struct { + min int + max int + outputOriginal bool + separator string + filler string + input analysis.TokenStream + output analysis.TokenStream + }{ + { + min: 2, + max: 2, + outputOriginal: false, + separator: " ", + filler: "_", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("quick"), + }, + &analysis.Token{ + Term: []byte("brown"), + }, + &analysis.Token{ + Term: []byte("fox"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the quick"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("quick brown"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("brown fox"), + Type: analysis.Shingle, + }, + }, + }, + { + min: 3, + max: 3, + outputOriginal: false, + separator: " ", + filler: "_", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("quick"), + }, + &analysis.Token{ + Term: []byte("brown"), + }, + &analysis.Token{ + Term: []byte("fox"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the quick brown"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("quick brown fox"), + Type: analysis.Shingle, + }, + }, + }, + { + min: 2, + max: 3, + outputOriginal: false, + separator: " ", + filler: "_", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("quick"), + }, + &analysis.Token{ + Term: []byte("brown"), + }, + &analysis.Token{ + Term: []byte("fox"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the quick"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("quick brown"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("the quick brown"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("brown fox"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("quick brown fox"), + Type: analysis.Shingle, + }, + }, + }, + { + min: 3, + max: 3, + outputOriginal: false, + separator: " ", + filler: "_", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ugly"), + Position: 1, + }, + &analysis.Token{ + Term: []byte("quick"), + Position: 3, + }, + &analysis.Token{ + Term: []byte("brown"), + Position: 4, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ugly _ quick"), + Type: analysis.Shingle, + Position: 1, + }, + &analysis.Token{ + Term: []byte("_ quick brown"), + Type: analysis.Shingle, + Position: 3, + }, + }, + }, + { + min: 1, + max: 5, + outputOriginal: false, + separator: " ", + filler: "_", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("test"), + Position: 1, + }, + &analysis.Token{ + Term: []byte("text"), + Position: 2, + }, + // token 3 removed by stop filter + &analysis.Token{ + Term: []byte("see"), + Position: 4, + }, + &analysis.Token{ + Term: []byte("shingles"), + Position: 5, + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("test"), + Type: analysis.Shingle, + Position: 1, + }, + &analysis.Token{ + Term: []byte("text"), + Type: analysis.Shingle, + Position: 2, + }, + &analysis.Token{ + Term: []byte("test text"), + Type: analysis.Shingle, + Position: 1, + }, + &analysis.Token{ + Term: []byte("_"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("text _"), + Type: analysis.Shingle, + Position: 2, + }, + &analysis.Token{ + Term: []byte("test text _"), + Type: analysis.Shingle, + Position: 1, + }, + &analysis.Token{ + Term: []byte("see"), + Type: analysis.Shingle, + Position: 4, + }, + &analysis.Token{ + Term: []byte("_ see"), + Type: analysis.Shingle, + Position: 4, + }, + &analysis.Token{ + Term: []byte("text _ see"), + Type: analysis.Shingle, + Position: 2, + }, + &analysis.Token{ + Term: []byte("test text _ see"), + Type: analysis.Shingle, + Position: 1, + }, + &analysis.Token{ + Term: []byte("shingles"), + Type: analysis.Shingle, + Position: 5, + }, + &analysis.Token{ + Term: []byte("see shingles"), + Type: analysis.Shingle, + Position: 4, + }, + &analysis.Token{ + Term: []byte("_ see shingles"), + Type: analysis.Shingle, + Position: 4, + }, + &analysis.Token{ + Term: []byte("text _ see shingles"), + Type: analysis.Shingle, + Position: 2, + }, + &analysis.Token{ + Term: []byte("test text _ see shingles"), + Type: analysis.Shingle, + Position: 1, + }, + }, + }, + { + min: 2, + max: 2, + outputOriginal: true, + separator: " ", + filler: "_", + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("quick"), + }, + &analysis.Token{ + Term: []byte("brown"), + }, + &analysis.Token{ + Term: []byte("fox"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("quick"), + }, + &analysis.Token{ + Term: []byte("the quick"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("brown"), + }, + &analysis.Token{ + Term: []byte("quick brown"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("fox"), + }, + &analysis.Token{ + Term: []byte("brown fox"), + Type: analysis.Shingle, + }, + }, + }, + } + + for _, test := range tests { + shingleFilter := NewShingleFilter(test.min, test.max, test.outputOriginal, test.separator, test.filler) + actual := shingleFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + } +} + +// TestShingleFilterBug431 tests that the shingle filter is in fact stateless +// by making using the same filter instance twice and ensuring we do not get +// contaminated output +func TestShingleFilterBug431(t *testing.T) { + + tests := []struct { + input analysis.TokenStream + output analysis.TokenStream + }{ + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("quick"), + }, + &analysis.Token{ + Term: []byte("brown"), + }, + &analysis.Token{ + Term: []byte("fox"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("the quick"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("quick brown"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("brown fox"), + Type: analysis.Shingle, + }, + }, + }, + { + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("sad"), + }, + &analysis.Token{ + Term: []byte("dirty"), + }, + &analysis.Token{ + Term: []byte("sock"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a sad"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("sad dirty"), + Type: analysis.Shingle, + }, + &analysis.Token{ + Term: []byte("dirty sock"), + Type: analysis.Shingle, + }, + }, + }, + } + + shingleFilter := NewShingleFilter(2, 2, false, " ", "_") + for _, test := range tests { + actual := shingleFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output, actual) + } + } + +} diff --git a/analysis/token/snowball/snowball.go b/analysis/token/snowball/snowball.go new file mode 100644 index 0000000..02c1a6c --- /dev/null +++ b/analysis/token/snowball/snowball.go @@ -0,0 +1,62 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package snowball + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + + "github.com/blevesearch/snowball" +) + +const Name = "stemmer_snowball" + +type SnowballStemmer struct { + language string +} + +func NewSnowballStemmer(language string) *SnowballStemmer { + return &SnowballStemmer{ + language: language, + } +} + +func (s *SnowballStemmer) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + // if it is not a protected keyword, stem it + if !token.KeyWord { + stemmed, _ := snowball.Stem(string(token.Term), s.language, true) + token.Term = []byte(stemmed) + } + } + return input +} + +func SnowballStemmerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + language, ok := config["language"].(string) + if !ok { + return nil, fmt.Errorf("must specify language") + } + return NewSnowballStemmer(language), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, SnowballStemmerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/snowball/snowball_test.go b/analysis/token/snowball/snowball_test.go new file mode 100644 index 0000000..a805347 --- /dev/null +++ b/analysis/token/snowball/snowball_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package snowball + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestSnowballStemmer(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("walking"), + }, + &analysis.Token{ + Term: []byte("talked"), + }, + &analysis.Token{ + Term: []byte("business"), + }, + &analysis.Token{ + Term: []byte("protected"), + KeyWord: true, + }, + &analysis.Token{ + Term: []byte("cat"), + }, + &analysis.Token{ + Term: []byte("done"), + }, + // a term which does stem, but does not change length + &analysis.Token{ + Term: []byte("marty"), + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("walk"), + }, + &analysis.Token{ + Term: []byte("talk"), + }, + &analysis.Token{ + Term: []byte("busi"), + }, + &analysis.Token{ + Term: []byte("protected"), + KeyWord: true, + }, + &analysis.Token{ + Term: []byte("cat"), + }, + &analysis.Token{ + Term: []byte("done"), + }, + &analysis.Token{ + Term: []byte("marti"), + }, + } + + filter := NewSnowballStemmer("english") + ouputTokenStream := filter.Filter(inputTokenStream) + if !reflect.DeepEqual(ouputTokenStream, expectedTokenStream) { + t.Errorf("expected %#v got %#v", expectedTokenStream[3], ouputTokenStream[3]) + } +} + +func BenchmarkSnowballStemmer(b *testing.B) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("walking"), + }, + &analysis.Token{ + Term: []byte("talked"), + }, + &analysis.Token{ + Term: []byte("business"), + }, + &analysis.Token{ + Term: []byte("protected"), + KeyWord: true, + }, + &analysis.Token{ + Term: []byte("cat"), + }, + &analysis.Token{ + Term: []byte("done"), + }, + } + + filter := NewSnowballStemmer("english") + b.ResetTimer() + + for i := 0; i < b.N; i++ { + filter.Filter(inputTokenStream) + } + +} diff --git a/analysis/token/stop/stop.go b/analysis/token/stop/stop.go new file mode 100644 index 0000000..09f2d1c --- /dev/null +++ b/analysis/token/stop/stop.go @@ -0,0 +1,73 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package stop implements a TokenFilter removing tokens found in +// a TokenMap. +// +// It constructor takes the following arguments: +// +// "stop_token_map" (string): the name of the token map identifying tokens to +// remove. +package stop + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "stop_tokens" + +type StopTokensFilter struct { + stopTokens analysis.TokenMap +} + +func NewStopTokensFilter(stopTokens analysis.TokenMap) *StopTokensFilter { + return &StopTokensFilter{ + stopTokens: stopTokens, + } +} + +func (f *StopTokensFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + j := 0 + for _, token := range input { + _, isStopToken := f.stopTokens[string(token.Term)] + if !isStopToken { + input[j] = token + j++ + } + } + + return input[:j] +} + +func StopTokensFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + stopTokenMapName, ok := config["stop_token_map"].(string) + if !ok { + return nil, fmt.Errorf("must specify stop_token_map") + } + stopTokenMap, err := cache.TokenMapNamed(stopTokenMapName) + if err != nil { + return nil, fmt.Errorf("error building stop words filter: %v", err) + } + return NewStopTokensFilter(stopTokenMap), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, StopTokensFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/stop/stop_test.go b/analysis/token/stop/stop_test.go new file mode 100644 index 0000000..f63c562 --- /dev/null +++ b/analysis/token/stop/stop_test.go @@ -0,0 +1,124 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stop + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenmap" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestStopWordsFilter(t *testing.T) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("walk"), + }, + &analysis.Token{ + Term: []byte("in"), + }, + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("park"), + }, + } + + expectedTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("walk"), + }, + &analysis.Token{ + Term: []byte("park"), + }, + } + + cache := registry.NewCache() + stopListConfig := map[string]interface{}{ + "type": tokenmap.Name, + "tokens": []interface{}{"a", "in", "the"}, + } + _, err := cache.DefineTokenMap("stop_test", stopListConfig) + if err != nil { + t.Fatal(err) + } + + stopConfig := map[string]interface{}{ + "type": "stop_tokens", + "stop_token_map": "stop_test", + } + stopFilter, err := cache.DefineTokenFilter("stop_test", stopConfig) + if err != nil { + t.Fatal(err) + } + + ouputTokenStream := stopFilter.Filter(inputTokenStream) + if !reflect.DeepEqual(ouputTokenStream, expectedTokenStream) { + t.Errorf("expected %#v got %#v", expectedTokenStream, ouputTokenStream) + } +} + +func BenchmarkStopWordsFilter(b *testing.B) { + + inputTokenStream := analysis.TokenStream{ + &analysis.Token{ + Term: []byte("a"), + }, + &analysis.Token{ + Term: []byte("walk"), + }, + &analysis.Token{ + Term: []byte("in"), + }, + &analysis.Token{ + Term: []byte("the"), + }, + &analysis.Token{ + Term: []byte("park"), + }, + } + + cache := registry.NewCache() + stopListConfig := map[string]interface{}{ + "type": tokenmap.Name, + "tokens": []interface{}{"a", "in", "the"}, + } + _, err := cache.DefineTokenMap("stop_test", stopListConfig) + if err != nil { + b.Fatal(err) + } + + stopConfig := map[string]interface{}{ + "type": "stop_tokens", + "stop_token_map": "stop_test", + } + stopFilter, err := cache.DefineTokenFilter("stop_test", stopConfig) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stopFilter.Filter(inputTokenStream) + } + +} diff --git a/analysis/token/truncate/truncate.go b/analysis/token/truncate/truncate.go new file mode 100644 index 0000000..2019358 --- /dev/null +++ b/analysis/token/truncate/truncate.go @@ -0,0 +1,62 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package truncate + +import ( + "fmt" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "truncate_token" + +type TruncateTokenFilter struct { + length int +} + +func NewTruncateTokenFilter(length int) *TruncateTokenFilter { + return &TruncateTokenFilter{ + length: length, + } +} + +func (s *TruncateTokenFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + wordLen := utf8.RuneCount(token.Term) + if wordLen > s.length { + token.Term = analysis.TruncateRunes(token.Term, wordLen-s.length) + } + } + return input +} + +func TruncateTokenFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + lenVal, ok := config["length"].(float64) + if !ok { + return nil, fmt.Errorf("must specify length") + } + length := int(lenVal) + + return NewTruncateTokenFilter(length), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, TruncateTokenFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/truncate/truncate_test.go b/analysis/token/truncate/truncate_test.go new file mode 100644 index 0000000..083e277 --- /dev/null +++ b/analysis/token/truncate/truncate_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package truncate + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestTruncateTokenFilter(t *testing.T) { + + tests := []struct { + length int + input analysis.TokenStream + output analysis.TokenStream + }{ + { + length: 5, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcdefgh"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("abcde"), + }, + }, + }, + { + length: 3, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こんにちは世界"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("こんに"), + }, + }, + }, + { + length: 10, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("แยกคำภาษาไทยก็ทำได้นะจ้ะ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("แยกคำภาษาไ"), + }, + }, + }, + } + + for _, test := range tests { + truncateTokenFilter := NewTruncateTokenFilter(test.length) + actual := truncateTokenFilter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/token/unicodenorm/unicodenorm.go b/analysis/token/unicodenorm/unicodenorm.go new file mode 100644 index 0000000..ba0243a --- /dev/null +++ b/analysis/token/unicodenorm/unicodenorm.go @@ -0,0 +1,82 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unicodenorm + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" + "golang.org/x/text/unicode/norm" +) + +const Name = "normalize_unicode" + +const NFC = "nfc" +const NFD = "nfd" +const NFKC = "nfkc" +const NFKD = "nfkd" + +var forms = map[string]norm.Form{ + NFC: norm.NFC, + NFD: norm.NFD, + NFKC: norm.NFKC, + NFKD: norm.NFKD, +} + +type UnicodeNormalizeFilter struct { + form norm.Form +} + +func NewUnicodeNormalizeFilter(formName string) (*UnicodeNormalizeFilter, error) { + form, ok := forms[formName] + if !ok { + return nil, fmt.Errorf("no form named %s", formName) + } + return &UnicodeNormalizeFilter{ + form: form, + }, nil +} + +func MustNewUnicodeNormalizeFilter(formName string) *UnicodeNormalizeFilter { + filter, err := NewUnicodeNormalizeFilter(formName) + if err != nil { + panic(err) + } + return filter +} + +func (s *UnicodeNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + for _, token := range input { + token.Term = s.form.Bytes(token.Term) + } + return input +} + +func UnicodeNormalizeFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + formVal, ok := config["form"].(string) + if !ok { + return nil, fmt.Errorf("must specify form") + } + form := formVal + return NewUnicodeNormalizeFilter(form) +} + +func init() { + err := registry.RegisterTokenFilter(Name, UnicodeNormalizeFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/unicodenorm/unicodenorm_test.go b/analysis/token/unicodenorm/unicodenorm_test.go new file mode 100644 index 0000000..0ff2bd8 --- /dev/null +++ b/analysis/token/unicodenorm/unicodenorm_test.go @@ -0,0 +1,162 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unicodenorm + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +// the following tests come from the lucene +// test cases for CJK width filter +// which is our basis for using this +// as a substitute for that +func TestUnicodeNormalization(t *testing.T) { + + tests := []struct { + formName string + input analysis.TokenStream + output analysis.TokenStream + }{ + { + formName: NFKD, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Test"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("Test"), + }, + }, + }, + { + formName: NFKD, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1234"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("1234"), + }, + }, + }, + { + formName: NFKD, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("カタカナ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("カタカナ"), + }, + }, + }, + { + formName: NFKC, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ヴィッツ"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("ヴィッツ"), + }, + }, + }, + { + formName: NFKC, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("パナソニック"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("パナソニック"), + }, + }, + }, + { + formName: NFD, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u212B"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0041\u030A"), + }, + }, + }, + { + formName: NFC, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u212B"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u00C5"), + }, + }, + }, + { + formName: NFKD, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\uFB01"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0066\u0069"), + }, + }, + }, + { + formName: NFKC, + input: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\uFB01"), + }, + }, + output: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("\u0066\u0069"), + }, + }, + }, + } + + for _, test := range tests { + filter := MustNewUnicodeNormalizeFilter(test.formName) + actual := filter.Filter(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %s, got %s", test.output[0].Term, actual[0].Term) + t.Errorf("expected %#v, got %#v", test.output[0].Term, actual[0].Term) + } + } +} diff --git a/analysis/token/unique/unique.go b/analysis/token/unique/unique.go new file mode 100644 index 0000000..e48c4f9 --- /dev/null +++ b/analysis/token/unique/unique.go @@ -0,0 +1,56 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unique + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "unique" + +// UniqueTermFilter retains only the tokens which mark the first occurrence of +// a term. Tokens whose term appears in a preceding token are dropped. +type UniqueTermFilter struct{} + +func NewUniqueTermFilter() *UniqueTermFilter { + return &UniqueTermFilter{} +} + +func (f *UniqueTermFilter) Filter(input analysis.TokenStream) analysis.TokenStream { + encounteredTerms := make(map[string]struct{}, len(input)/4) + j := 0 + for _, token := range input { + term := string(token.Term) + if _, ok := encounteredTerms[term]; ok { + continue + } + encounteredTerms[term] = struct{}{} + input[j] = token + j++ + } + return input[:j] +} + +func UniqueTermFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { + return NewUniqueTermFilter(), nil +} + +func init() { + err := registry.RegisterTokenFilter(Name, UniqueTermFilterConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/token/unique/unique_test.go b/analysis/token/unique/unique_test.go new file mode 100644 index 0000000..bc17130 --- /dev/null +++ b/analysis/token/unique/unique_test.go @@ -0,0 +1,84 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unique + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestUniqueTermFilter(t *testing.T) { + var tests = []struct { + input analysis.TokenStream + // expected indices of input which should be included in the output. We + // use indices instead of another TokenStream, since position/start/end + // should be preserved. + expectedIndices []int + }{ + { + input: tokenStream(), + expectedIndices: []int{}, + }, + { + input: tokenStream("a"), + expectedIndices: []int{0}, + }, + { + input: tokenStream("each", "term", "in", "this", "sentence", "is", "unique"), + expectedIndices: []int{0, 1, 2, 3, 4, 5, 6}, + }, + { + input: tokenStream("Lui", "è", "alto", "e", "lei", "è", "bassa"), + expectedIndices: []int{0, 1, 2, 3, 4, 6}, + }, + { + input: tokenStream("a", "a", "A", "a", "a", "A"), + expectedIndices: []int{0, 2}, + }, + } + uniqueTermFilter := NewUniqueTermFilter() + for _, test := range tests { + expected := subStream(test.input, test.expectedIndices) + actual := uniqueTermFilter.Filter(test.input) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected %s \n\n got %s", expected, actual) + } + } +} + +func tokenStream(termStrs ...string) analysis.TokenStream { + tokenStream := make([]*analysis.Token, len(termStrs)) + index := 0 + for i, termStr := range termStrs { + tokenStream[i] = &analysis.Token{ + Term: []byte(termStr), + Position: i + 1, + Start: index, + End: index + len(termStr), + } + index += len(termStr) + } + return analysis.TokenStream(tokenStream) +} + +func subStream(stream analysis.TokenStream, indices []int) analysis.TokenStream { + result := make(analysis.TokenStream, len(indices)) + for i, index := range indices { + result[i] = stream[index] + } + return result +} diff --git a/analysis/tokenizer/character/character.go b/analysis/tokenizer/character/character.go new file mode 100644 index 0000000..9c4f7e4 --- /dev/null +++ b/analysis/tokenizer/character/character.go @@ -0,0 +1,76 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package character + +import ( + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/analysis" +) + +type IsTokenRune func(r rune) bool + +type CharacterTokenizer struct { + isTokenRun IsTokenRune +} + +func NewCharacterTokenizer(f IsTokenRune) *CharacterTokenizer { + return &CharacterTokenizer{ + isTokenRun: f, + } +} + +func (c *CharacterTokenizer) Tokenize(input []byte) analysis.TokenStream { + + rv := make(analysis.TokenStream, 0, 1024) + + offset := 0 + start := 0 + end := 0 + count := 0 + for currRune, size := utf8.DecodeRune(input[offset:]); currRune != utf8.RuneError; currRune, size = utf8.DecodeRune(input[offset:]) { + isToken := c.isTokenRun(currRune) + if isToken { + end = offset + size + } else { + if end-start > 0 { + // build token + rv = append(rv, &analysis.Token{ + Term: input[start:end], + Start: start, + End: end, + Position: count + 1, + Type: analysis.AlphaNumeric, + }) + count++ + } + start = offset + size + end = start + } + offset += size + } + // if we ended in the middle of a token, finish it + if end-start > 0 { + // build token + rv = append(rv, &analysis.Token{ + Term: input[start:end], + Start: start, + End: end, + Position: count + 1, + Type: analysis.AlphaNumeric, + }) + } + return rv +} diff --git a/analysis/tokenizer/character/character_test.go b/analysis/tokenizer/character/character_test.go new file mode 100644 index 0000000..30d00e8 --- /dev/null +++ b/analysis/tokenizer/character/character_test.go @@ -0,0 +1,84 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package character + +import ( + "reflect" + "testing" + "unicode" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestCharacterTokenizer(t *testing.T) { + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + []byte("Hello World."), + analysis.TokenStream{ + { + Start: 0, + End: 5, + Term: []byte("Hello"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 6, + End: 11, + Term: []byte("World"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("dominique@mcdiabetes.com"), + analysis.TokenStream{ + { + Start: 0, + End: 9, + Term: []byte("dominique"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 10, + End: 20, + Term: []byte("mcdiabetes"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + { + Start: 21, + End: 24, + Term: []byte("com"), + Position: 3, + Type: analysis.AlphaNumeric, + }, + }, + }, + } + + tokenizer := NewCharacterTokenizer(unicode.IsLetter) + for _, test := range tests { + actual := tokenizer.Tokenize(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("Expected %v, got %v for %s", test.output, actual, string(test.input)) + } + } +} diff --git a/analysis/tokenizer/exception/exception.go b/analysis/tokenizer/exception/exception.go new file mode 100644 index 0000000..30c8eff --- /dev/null +++ b/analysis/tokenizer/exception/exception.go @@ -0,0 +1,144 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package exception implements a Tokenizer which extracts pieces matched by a +// regular expression from the input data, delegates the rest to another +// tokenizer, then insert back extracted parts in the token stream. Use it to +// preserve sequences which a regular tokenizer would alter or remove. +// +// Its constructor takes the following arguments: +// +// "exceptions" ([]string): one or more Go regular expressions matching the +// sequence to preserve. Multiple expressions are combined with "|". +// +// "tokenizer" (string): the name of the tokenizer processing the data not +// matched by "exceptions". +package exception + +import ( + "fmt" + "regexp" + "strings" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "exception" + +type ExceptionsTokenizer struct { + exception *regexp.Regexp + remaining analysis.Tokenizer +} + +func NewExceptionsTokenizer(exception *regexp.Regexp, remaining analysis.Tokenizer) *ExceptionsTokenizer { + return &ExceptionsTokenizer{ + exception: exception, + remaining: remaining, + } +} + +func (t *ExceptionsTokenizer) Tokenize(input []byte) analysis.TokenStream { + rv := make(analysis.TokenStream, 0) + matches := t.exception.FindAllIndex(input, -1) + currInput := 0 + lastPos := 0 + for _, match := range matches { + start := match[0] + end := match[1] + if start > currInput { + // need to defer to remaining for unprocessed section + intermediate := t.remaining.Tokenize(input[currInput:start]) + // add intermediate tokens to our result stream + for _, token := range intermediate { + // adjust token offsets + token.Position += lastPos + token.Start += currInput + token.End += currInput + rv = append(rv, token) + } + lastPos += len(intermediate) + currInput = start + } + + // create single token with this regexp match + token := &analysis.Token{ + Term: input[start:end], + Start: start, + End: end, + Position: lastPos + 1, + } + rv = append(rv, token) + lastPos++ + currInput = end + + } + + if currInput < len(input) { + // need to defer to remaining for unprocessed section + intermediate := t.remaining.Tokenize(input[currInput:]) + // add intermediate tokens to our result stream + for _, token := range intermediate { + // adjust token offsets + token.Position += lastPos + token.Start += currInput + token.End += currInput + rv = append(rv, token) + } + } + + return rv +} + +func ExceptionsTokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { + exceptions := []string{} + iexceptions, ok := config["exceptions"].([]interface{}) + if ok { + for _, exception := range iexceptions { + exception, ok := exception.(string) + if ok { + exceptions = append(exceptions, exception) + } + } + } + aexceptions, ok := config["exceptions"].([]string) + if ok { + exceptions = append(exceptions, aexceptions...) + } + if len(exceptions) == 0 { + return nil, fmt.Errorf("no pattern found in 'exception' property") + } + exceptionPattern := strings.Join(exceptions, "|") + r, err := regexp.Compile(exceptionPattern) + if err != nil { + return nil, fmt.Errorf("unable to build regexp tokenizer: %v", err) + } + + remainingName, ok := config["tokenizer"].(string) + if !ok { + return nil, fmt.Errorf("must specify tokenizer for remaining input") + } + remaining, err := cache.TokenizerNamed(remainingName) + if err != nil { + return nil, err + } + return NewExceptionsTokenizer(r, remaining), nil +} + +func init() { + err := registry.RegisterTokenizer(Name, ExceptionsTokenizerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/tokenizer/exception/exception_test.go b/analysis/tokenizer/exception/exception_test.go new file mode 100644 index 0000000..1c4acb0 --- /dev/null +++ b/analysis/tokenizer/exception/exception_test.go @@ -0,0 +1,171 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exception + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + _ "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestExceptionsTokenizer(t *testing.T) { + tests := []struct { + config map[string]interface{} + input []byte + patterns []string + result analysis.TokenStream + }{ + { + input: []byte("test http://blevesearch.com/ words"), + config: map[string]interface{}{ + "type": "exception", + "tokenizer": "unicode", + "exceptions": []interface{}{ + `[hH][tT][tT][pP][sS]?://(\S)*`, + `[fF][iI][lL][eE]://(\S)*`, + `[fF][tT][pP]://(\S)*`, + }, + }, + result: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("test"), + Position: 1, + Start: 0, + End: 4, + }, + &analysis.Token{ + Term: []byte("http://blevesearch.com/"), + Position: 2, + Start: 5, + End: 28, + }, + &analysis.Token{ + Term: []byte("words"), + Position: 3, + Start: 29, + End: 34, + }, + }, + }, + { + input: []byte("what ftp://blevesearch.com/ songs"), + config: map[string]interface{}{ + "type": "exception", + "tokenizer": "unicode", + "exceptions": []interface{}{ + `[hH][tT][tT][pP][sS]?://(\S)*`, + `[fF][iI][lL][eE]://(\S)*`, + `[fF][tT][pP]://(\S)*`, + }, + }, + result: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("what"), + Position: 1, + Start: 0, + End: 4, + }, + &analysis.Token{ + Term: []byte("ftp://blevesearch.com/"), + Position: 2, + Start: 5, + End: 27, + }, + &analysis.Token{ + Term: []byte("songs"), + Position: 3, + Start: 28, + End: 33, + }, + }, + }, + { + input: []byte("please email marty@couchbase.com the URL https://blevesearch.com/"), + config: map[string]interface{}{ + "type": "exception", + "tokenizer": "unicode", + "exceptions": []interface{}{ + `[hH][tT][tT][pP][sS]?://(\S)*`, + `[fF][iI][lL][eE]://(\S)*`, + `[fF][tT][pP]://(\S)*`, + `\S+@\S+`, + }, + }, + result: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("please"), + Position: 1, + Start: 0, + End: 6, + }, + &analysis.Token{ + Term: []byte("email"), + Position: 2, + Start: 7, + End: 12, + }, + &analysis.Token{ + Term: []byte("marty@couchbase.com"), + Position: 3, + Start: 13, + End: 32, + }, + &analysis.Token{ + Term: []byte("the"), + Position: 4, + Start: 33, + End: 36, + }, + &analysis.Token{ + Term: []byte("URL"), + Position: 5, + Start: 37, + End: 40, + }, + &analysis.Token{ + Term: []byte("https://blevesearch.com/"), + Position: 6, + Start: 41, + End: 65, + }, + }, + }, + } + + // remaining := unicode.NewUnicodeTokenizer() + for _, test := range tests { + + // build the requested exception tokenizer + cache := registry.NewCache() + tokenizer, err := cache.DefineTokenizer("custom", test.config) + if err != nil { + t.Fatal(err) + } + + // pattern := strings.Join(test.patterns, "|") + // r, err := regexp.Compile(pattern) + // if err != nil { + // t.Fatal(err) + // } + // tokenizer := NewExceptionsTokenizer(r, remaining) + actual := tokenizer.Tokenize(test.input) + if !reflect.DeepEqual(actual, test.result) { + t.Errorf("expected %v, got %v", test.result, actual) + } + } +} diff --git a/analysis/tokenizer/letter/letter.go b/analysis/tokenizer/letter/letter.go new file mode 100644 index 0000000..ed4dba1 --- /dev/null +++ b/analysis/tokenizer/letter/letter.go @@ -0,0 +1,36 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package letter + +import ( + "unicode" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/character" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "letter" + +func TokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { + return character.NewCharacterTokenizer(unicode.IsLetter), nil +} + +func init() { + err := registry.RegisterTokenizer(Name, TokenizerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/tokenizer/regexp/regexp.go b/analysis/tokenizer/regexp/regexp.go new file mode 100644 index 0000000..94c1879 --- /dev/null +++ b/analysis/tokenizer/regexp/regexp.go @@ -0,0 +1,87 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package regexp + +import ( + "fmt" + "regexp" + "strconv" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "regexp" + +var IdeographRegexp = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) + +type RegexpTokenizer struct { + r *regexp.Regexp +} + +func NewRegexpTokenizer(r *regexp.Regexp) *RegexpTokenizer { + return &RegexpTokenizer{ + r: r, + } +} + +func (rt *RegexpTokenizer) Tokenize(input []byte) analysis.TokenStream { + matches := rt.r.FindAllIndex(input, -1) + rv := make(analysis.TokenStream, 0, len(matches)) + for i, match := range matches { + matchBytes := input[match[0]:match[1]] + if match[1]-match[0] > 0 { + token := analysis.Token{ + Term: matchBytes, + Start: match[0], + End: match[1], + Position: i + 1, + Type: detectTokenType(matchBytes), + } + rv = append(rv, &token) + } + } + return rv +} + +func RegexpTokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { + rval, ok := config["regexp"].(string) + if !ok { + return nil, fmt.Errorf("must specify regexp") + } + r, err := regexp.Compile(rval) + if err != nil { + return nil, fmt.Errorf("unable to build regexp tokenizer: %v", err) + } + return NewRegexpTokenizer(r), nil +} + +func init() { + err := registry.RegisterTokenizer(Name, RegexpTokenizerConstructor) + if err != nil { + panic(err) + } +} + +func detectTokenType(termBytes []byte) analysis.TokenType { + if IdeographRegexp.Match(termBytes) { + return analysis.Ideographic + } + _, err := strconv.ParseFloat(string(termBytes), 64) + if err == nil { + return analysis.Numeric + } + return analysis.AlphaNumeric +} diff --git a/analysis/tokenizer/regexp/regexp_test.go b/analysis/tokenizer/regexp/regexp_test.go new file mode 100644 index 0000000..cfad915 --- /dev/null +++ b/analysis/tokenizer/regexp/regexp_test.go @@ -0,0 +1,166 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package regexp + +import ( + "reflect" + "regexp" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestBoundary(t *testing.T) { + + wordRegex := regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}|\w+`) + + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + []byte("Hello World."), + analysis.TokenStream{ + { + Start: 0, + End: 5, + Term: []byte("Hello"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 6, + End: 11, + Term: []byte("World"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("こんにちは世界"), + analysis.TokenStream{ + { + Start: 0, + End: 3, + Term: []byte("こ"), + Position: 1, + Type: analysis.Ideographic, + }, + { + Start: 3, + End: 6, + Term: []byte("ん"), + Position: 2, + Type: analysis.Ideographic, + }, + { + Start: 6, + End: 9, + Term: []byte("に"), + Position: 3, + Type: analysis.Ideographic, + }, + { + Start: 9, + End: 12, + Term: []byte("ち"), + Position: 4, + Type: analysis.Ideographic, + }, + { + Start: 12, + End: 15, + Term: []byte("は"), + Position: 5, + Type: analysis.Ideographic, + }, + { + Start: 15, + End: 18, + Term: []byte("世"), + Position: 6, + Type: analysis.Ideographic, + }, + { + Start: 18, + End: 21, + Term: []byte("界"), + Position: 7, + Type: analysis.Ideographic, + }, + }, + }, + { + []byte(""), + analysis.TokenStream{}, + }, + } + + for _, test := range tests { + tokenizer := NewRegexpTokenizer(wordRegex) + actual := tokenizer.Tokenize(test.input) + + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("Expected %v, got %v for %s", test.output, actual, string(test.input)) + } + } +} + +func TestBugProducingEmptyTokens(t *testing.T) { + + wordRegex := regexp.MustCompile(`[0-9a-zA-Z_]*`) + + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + []byte("Chatha Edwards Sr."), + analysis.TokenStream{ + { + Start: 0, + End: 6, + Term: []byte("Chatha"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 7, + End: 14, + Term: []byte("Edwards"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + { + Start: 15, + End: 17, + Term: []byte("Sr"), + Position: 3, + Type: analysis.AlphaNumeric, + }, + }, + }, + } + + for _, test := range tests { + tokenizer := NewRegexpTokenizer(wordRegex) + actual := tokenizer.Tokenize(test.input) + + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("Expected %v, got %v for %s", test.output, actual, string(test.input)) + } + } +} diff --git a/analysis/tokenizer/single/single.go b/analysis/tokenizer/single/single.go new file mode 100644 index 0000000..7f3abd2 --- /dev/null +++ b/analysis/tokenizer/single/single.go @@ -0,0 +1,52 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package single + +import ( + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "single" + +type SingleTokenTokenizer struct { +} + +func NewSingleTokenTokenizer() *SingleTokenTokenizer { + return &SingleTokenTokenizer{} +} + +func (t *SingleTokenTokenizer) Tokenize(input []byte) analysis.TokenStream { + return analysis.TokenStream{ + &analysis.Token{ + Term: input, + Position: 1, + Start: 0, + End: len(input), + Type: analysis.AlphaNumeric, + }, + } +} + +func SingleTokenTokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { + return NewSingleTokenTokenizer(), nil +} + +func init() { + err := registry.RegisterTokenizer(Name, SingleTokenTokenizerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/tokenizer/single/single_test.go b/analysis/tokenizer/single/single_test.go new file mode 100644 index 0000000..7cc134c --- /dev/null +++ b/analysis/tokenizer/single/single_test.go @@ -0,0 +1,76 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package single + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestSingleTokenTokenizer(t *testing.T) { + + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + []byte("Hello World"), + analysis.TokenStream{ + { + Start: 0, + End: 11, + Term: []byte("Hello World"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("こんにちは世界"), + analysis.TokenStream{ + { + Start: 0, + End: 21, + Term: []byte("こんにちは世界"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("แยกคำภาษาไทยก็ทำได้นะจ้ะ"), + analysis.TokenStream{ + { + Start: 0, + End: 72, + Term: []byte("แยกคำภาษาไทยก็ทำได้นะจ้ะ"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + }, + }, + } + + for _, test := range tests { + tokenizer := NewSingleTokenTokenizer() + actual := tokenizer.Tokenize(test.input) + + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("Expected %v, got %v for %s", test.output, actual, string(test.input)) + } + } +} diff --git a/analysis/tokenizer/unicode/unicode.go b/analysis/tokenizer/unicode/unicode.go new file mode 100644 index 0000000..b694a3e --- /dev/null +++ b/analysis/tokenizer/unicode/unicode.go @@ -0,0 +1,134 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unicode + +import ( + "github.com/blevesearch/segment" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "unicode" + +type UnicodeTokenizer struct { +} + +func NewUnicodeTokenizer() *UnicodeTokenizer { + return &UnicodeTokenizer{} +} + +func (rt *UnicodeTokenizer) Tokenize(input []byte) analysis.TokenStream { + rvx := make([]analysis.TokenStream, 0, 10) // When rv gets full, append to rvx. + rv := make(analysis.TokenStream, 0, 1) + + ta := []analysis.Token(nil) + taNext := 0 + + segmenter := segment.NewWordSegmenterDirect(input) + start := 0 + pos := 1 + + guessRemaining := func(end int) int { + avgSegmentLen := end / (len(rv) + 1) + if avgSegmentLen < 1 { + avgSegmentLen = 1 + } + + remainingLen := len(input) - end + + return remainingLen / avgSegmentLen + } + + for segmenter.Segment() { + segmentBytes := segmenter.Bytes() + end := start + len(segmentBytes) + if segmenter.Type() != segment.None { + if taNext >= len(ta) { + remainingSegments := guessRemaining(end) + if remainingSegments > 1000 { + remainingSegments = 1000 + } + if remainingSegments < 1 { + remainingSegments = 1 + } + + ta = make([]analysis.Token, remainingSegments) + taNext = 0 + } + + token := &ta[taNext] + taNext++ + + token.Term = segmentBytes + token.Start = start + token.End = end + token.Position = pos + token.Type = convertType(segmenter.Type()) + + if len(rv) >= cap(rv) { // When rv is full, save it into rvx. + rvx = append(rvx, rv) + + rvCap := cap(rv) * 2 + if rvCap > 256 { + rvCap = 256 + } + + rv = make(analysis.TokenStream, 0, rvCap) // Next rv cap is bigger. + } + + rv = append(rv, token) + pos++ + } + start = end + } + + if len(rvx) > 0 { + n := len(rv) + for _, r := range rvx { + n += len(r) + } + rall := make(analysis.TokenStream, 0, n) + for _, r := range rvx { + rall = append(rall, r...) + } + return append(rall, rv...) + } + + return rv +} + +func UnicodeTokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { + return NewUnicodeTokenizer(), nil +} + +func init() { + err := registry.RegisterTokenizer(Name, UnicodeTokenizerConstructor) + if err != nil { + panic(err) + } +} + +func convertType(segmentWordType int) analysis.TokenType { + switch segmentWordType { + case segment.Ideo: + return analysis.Ideographic + case segment.Kana: + return analysis.Ideographic + case segment.Number: + return analysis.Numeric + } + return analysis.AlphaNumeric +} diff --git a/analysis/tokenizer/unicode/unicode_test.go b/analysis/tokenizer/unicode/unicode_test.go new file mode 100644 index 0000000..0e0cfd4 --- /dev/null +++ b/analysis/tokenizer/unicode/unicode_test.go @@ -0,0 +1,202 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unicode + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/segment" +) + +func TestUnicode(t *testing.T) { + + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + []byte("Hello World"), + analysis.TokenStream{ + { + Start: 0, + End: 5, + Term: []byte("Hello"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 6, + End: 11, + Term: []byte("World"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("steven's"), + analysis.TokenStream{ + { + Start: 0, + End: 8, + Term: []byte("steven's"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("こんにちは世界"), + analysis.TokenStream{ + { + Start: 0, + End: 3, + Term: []byte("こ"), + Position: 1, + Type: analysis.Ideographic, + }, + { + Start: 3, + End: 6, + Term: []byte("ん"), + Position: 2, + Type: analysis.Ideographic, + }, + { + Start: 6, + End: 9, + Term: []byte("に"), + Position: 3, + Type: analysis.Ideographic, + }, + { + Start: 9, + End: 12, + Term: []byte("ち"), + Position: 4, + Type: analysis.Ideographic, + }, + { + Start: 12, + End: 15, + Term: []byte("は"), + Position: 5, + Type: analysis.Ideographic, + }, + { + Start: 15, + End: 18, + Term: []byte("世"), + Position: 6, + Type: analysis.Ideographic, + }, + { + Start: 18, + End: 21, + Term: []byte("界"), + Position: 7, + Type: analysis.Ideographic, + }, + }, + }, + { + []byte("age 25"), + analysis.TokenStream{ + { + Start: 0, + End: 3, + Term: []byte("age"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 4, + End: 6, + Term: []byte("25"), + Position: 2, + Type: analysis.Numeric, + }, + }, + }, + { + []byte("カ"), + analysis.TokenStream{ + { + Start: 0, + End: 3, + Term: []byte("カ"), + Position: 1, + Type: analysis.Ideographic, + }, + }, + }, + } + + for _, test := range tests { + tokenizer := NewUnicodeTokenizer() + actual := tokenizer.Tokenize(test.input) + + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("Expected %v, got %v for %s", test.output, actual, string(test.input)) + } + } +} + +var sampleLargeInput = []byte(`There are three characteristics of liquids which are relevant to the discussion of a BLEVE: +If a liquid in a sealed container is boiled, the pressure inside the container increases. As the liquid changes to a gas it expands - this expansion in a vented container would cause the gas and liquid to take up more space. In a sealed container the gas and liquid are not able to take up more space and so the pressure rises. Pressurized vessels containing liquids can reach an equilibrium where the liquid stops boiling and the pressure stops rising. This occurs when no more heat is being added to the system (either because it has reached ambient temperature or has had a heat source removed). +The boiling temperature of a liquid is dependent on pressure - high pressures will yield high boiling temperatures, and low pressures will yield low boiling temperatures. A common simple experiment is to place a cup of water in a vacuum chamber, and then reduce the pressure in the chamber until the water boils. By reducing the pressure the water will boil even at room temperature. This works both ways - if the pressure is increased beyond normal atmospheric pressures, the boiling of hot water could be suppressed far beyond normal temperatures. The cooling system of a modern internal combustion engine is a real-world example. +When a liquid boils it turns into a gas. The resulting gas takes up far more space than the liquid did. +Typically, a BLEVE starts with a container of liquid which is held above its normal, atmospheric-pressure boiling temperature. Many substances normally stored as liquids, such as CO2, oxygen, and other similar industrial gases have boiling temperatures, at atmospheric pressure, far below room temperature. In the case of water, a BLEVE could occur if a pressurized chamber of water is heated far beyond the standard 100 °C (212 °F). That container, because the boiling water pressurizes it, is capable of holding liquid water at very high temperatures. +If the pressurized vessel, containing liquid at high temperature (which may be room temperature, depending on the substance) ruptures, the pressure which prevents the liquid from boiling is lost. If the rupture is catastrophic, where the vessel is immediately incapable of holding any pressure at all, then there suddenly exists a large mass of liquid which is at very high temperature and very low pressure. This causes the entire volume of liquid to instantaneously boil, which in turn causes an extremely rapid expansion. Depending on temperatures, pressures and the substance involved, that expansion may be so rapid that it can be classified as an explosion, fully capable of inflicting severe damage on its surroundings.`) + +func BenchmarkTokenizeEnglishText(b *testing.B) { + + tokenizer := NewUnicodeTokenizer() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + tokenizer.Tokenize(sampleLargeInput) + } + +} + +func TestConvertType(t *testing.T) { + tests := []struct { + in int + out analysis.TokenType + }{ + { + segment.Ideo, analysis.Ideographic, + }, + { + segment.Kana, analysis.Ideographic, + }, + { + segment.Number, analysis.Numeric, + }, + { + segment.Letter, analysis.AlphaNumeric, + }, + } + + for _, test := range tests { + actual := convertType(test.in) + if actual != test.out { + t.Errorf("expected %d, got %d for %d", test.out, actual, test.in) + } + } +} diff --git a/analysis/tokenizer/web/web.go b/analysis/tokenizer/web/web.go new file mode 100644 index 0000000..a534f20 --- /dev/null +++ b/analysis/tokenizer/web/web.go @@ -0,0 +1,50 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package web + +import ( + "regexp" + "strings" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/exception" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "web" + +var email = `(?:[a-z0-9!#$%&'*+/=?^_` + "`" + `{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_` + "`" + `{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])` +var url = `(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s` + "`" + `!()\[\]{};:'".,<>?«»“”‘’]))` +var twitterHandle = `@([a-zA-Z0-9_]){1,15}` +var twitterHashtag = `#([a-zA-Z0-9_])+` +var exceptions = []string{email, url, twitterHandle, twitterHashtag} + +var exceptionsRegexp = regexp.MustCompile(strings.Join(exceptions, "|")) + +func TokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { + remainingTokenizer, err := cache.TokenizerNamed(unicode.Name) + if err != nil { + return nil, err + } + return exception.NewExceptionsTokenizer(exceptionsRegexp, remainingTokenizer), nil +} + +func init() { + err := registry.RegisterTokenizer(Name, TokenizerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/tokenizer/web/web_test.go b/analysis/tokenizer/web/web_test.go new file mode 100644 index 0000000..d5dbcce --- /dev/null +++ b/analysis/tokenizer/web/web_test.go @@ -0,0 +1,148 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package web + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +func TestWeb(t *testing.T) { + + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + []byte("Hello info@blevesearch.com"), + analysis.TokenStream{ + { + Start: 0, + End: 5, + Term: []byte("Hello"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 6, + End: 26, + Term: []byte("info@blevesearch.com"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("That http://blevesearch.com"), + analysis.TokenStream{ + { + Start: 0, + End: 4, + Term: []byte("That"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 5, + End: 27, + Term: []byte("http://blevesearch.com"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("Hey @blevesearch"), + analysis.TokenStream{ + { + Start: 0, + End: 3, + Term: []byte("Hey"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 4, + End: 16, + Term: []byte("@blevesearch"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("This #bleve"), + analysis.TokenStream{ + { + Start: 0, + End: 4, + Term: []byte("This"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 5, + End: 11, + Term: []byte("#bleve"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("What about @blevesearch?"), + analysis.TokenStream{ + { + Start: 0, + End: 4, + Term: []byte("What"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 5, + End: 10, + Term: []byte("about"), + Position: 2, + Type: analysis.AlphaNumeric, + }, + { + Start: 11, + End: 23, + Term: []byte("@blevesearch"), + Position: 3, + Type: analysis.AlphaNumeric, + }, + }, + }, + } + + cache := registry.NewCache() + tokenizer, err := cache.TokenizerNamed(Name) + if err != nil { + t.Fatal(err) + } + + for _, test := range tests { + + actual := tokenizer.Tokenize(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("Expected %v, got %v for %s", test.output, actual, string(test.input)) + } + } +} diff --git a/analysis/tokenizer/whitespace/whitespace.go b/analysis/tokenizer/whitespace/whitespace.go new file mode 100644 index 0000000..d8b14bb --- /dev/null +++ b/analysis/tokenizer/whitespace/whitespace.go @@ -0,0 +1,40 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package whitespace + +import ( + "unicode" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/character" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "whitespace" + +func TokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { + return character.NewCharacterTokenizer(notSpace), nil +} + +func notSpace(r rune) bool { + return !unicode.IsSpace(r) +} + +func init() { + err := registry.RegisterTokenizer(Name, TokenizerConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/tokenizer/whitespace/whitespace_test.go b/analysis/tokenizer/whitespace/whitespace_test.go new file mode 100644 index 0000000..6bd6b54 --- /dev/null +++ b/analysis/tokenizer/whitespace/whitespace_test.go @@ -0,0 +1,106 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package whitespace + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/character" +) + +func TestBoundary(t *testing.T) { + + tests := []struct { + input []byte + output analysis.TokenStream + }{ + { + []byte("Hello World."), + analysis.TokenStream{ + { + Start: 0, + End: 5, + Term: []byte("Hello"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + { + Start: 6, + End: 12, + Term: []byte("World."), + Position: 2, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte("こんにちは世界"), + analysis.TokenStream{ + { + Start: 0, + End: 21, + Term: []byte("こんにちは世界"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + }, + }, + { + []byte(""), + analysis.TokenStream{}, + }, + { + []byte("abc界"), + analysis.TokenStream{ + { + Start: 0, + End: 6, + Term: []byte("abc界"), + Position: 1, + Type: analysis.AlphaNumeric, + }, + }, + }, + } + + for _, test := range tests { + tokenizer := character.NewCharacterTokenizer(notSpace) + actual := tokenizer.Tokenize(test.input) + + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("Expected %v, got %v for %s", test.output, actual, string(test.input)) + } + } +} + +var sampleLargeInput = []byte(`There are three characteristics of liquids which are relevant to the discussion of a BLEVE: +If a liquid in a sealed container is boiled, the pressure inside the container increases. As the liquid changes to a gas it expands - this expansion in a vented container would cause the gas and liquid to take up more space. In a sealed container the gas and liquid are not able to take up more space and so the pressure rises. Pressurized vessels containing liquids can reach an equilibrium where the liquid stops boiling and the pressure stops rising. This occurs when no more heat is being added to the system (either because it has reached ambient temperature or has had a heat source removed). +The boiling temperature of a liquid is dependent on pressure - high pressures will yield high boiling temperatures, and low pressures will yield low boiling temperatures. A common simple experiment is to place a cup of water in a vacuum chamber, and then reduce the pressure in the chamber until the water boils. By reducing the pressure the water will boil even at room temperature. This works both ways - if the pressure is increased beyond normal atmospheric pressures, the boiling of hot water could be suppressed far beyond normal temperatures. The cooling system of a modern internal combustion engine is a real-world example. +When a liquid boils it turns into a gas. The resulting gas takes up far more space than the liquid did. +Typically, a BLEVE starts with a container of liquid which is held above its normal, atmospheric-pressure boiling temperature. Many substances normally stored as liquids, such as CO2, oxygen, and other similar industrial gases have boiling temperatures, at atmospheric pressure, far below room temperature. In the case of water, a BLEVE could occur if a pressurized chamber of water is heated far beyond the standard 100 °C (212 °F). That container, because the boiling water pressurizes it, is capable of holding liquid water at very high temperatures. +If the pressurized vessel, containing liquid at high temperature (which may be room temperature, depending on the substance) ruptures, the pressure which prevents the liquid from boiling is lost. If the rupture is catastrophic, where the vessel is immediately incapable of holding any pressure at all, then there suddenly exists a large mass of liquid which is at very high temperature and very low pressure. This causes the entire volume of liquid to instantaneously boil, which in turn causes an extremely rapid expansion. Depending on temperatures, pressures and the substance involved, that expansion may be so rapid that it can be classified as an explosion, fully capable of inflicting severe damage on its surroundings.`) + +func BenchmarkTokenizeEnglishText(b *testing.B) { + + tokenizer := character.NewCharacterTokenizer(notSpace) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + tokenizer.Tokenize(sampleLargeInput) + } + +} diff --git a/analysis/tokenmap.go b/analysis/tokenmap.go new file mode 100644 index 0000000..aa4ea31 --- /dev/null +++ b/analysis/tokenmap.go @@ -0,0 +1,76 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis + +import ( + "bufio" + "bytes" + "io" + "os" + "strings" +) + +type TokenMap map[string]bool + +func NewTokenMap() TokenMap { + return make(TokenMap, 0) +} + +// LoadFile reads in a list of tokens from a text file, +// one per line. +// Comments are supported using `#` or `|` +func (t TokenMap) LoadFile(filename string) error { + data, err := os.ReadFile(filename) + if err != nil { + return err + } + return t.LoadBytes(data) +} + +// LoadBytes reads in a list of tokens from memory, +// one per line. +// Comments are supported using `#` or `|` +func (t TokenMap) LoadBytes(data []byte) error { + bytesReader := bytes.NewReader(data) + bufioReader := bufio.NewReader(bytesReader) + line, err := bufioReader.ReadString('\n') + for err == nil { + t.LoadLine(line) + line, err = bufioReader.ReadString('\n') + } + // if the err was EOF we still need to process the last value + if err == io.EOF { + t.LoadLine(line) + return nil + } + return err +} + +func (t TokenMap) LoadLine(line string) { + // find the start of a comment, if any + startComment := strings.IndexAny(line, "#|") + if startComment >= 0 { + line = line[:startComment] + } + + tokens := strings.Fields(line) + for _, token := range tokens { + t.AddToken(token) + } +} + +func (t TokenMap) AddToken(token string) { + t[token] = true +} diff --git a/analysis/tokenmap/custom.go b/analysis/tokenmap/custom.go new file mode 100644 index 0000000..9567852 --- /dev/null +++ b/analysis/tokenmap/custom.go @@ -0,0 +1,65 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package token_map implements a generic TokenMap, often used in conjunction +// with filters to remove or process specific tokens. +// +// Its constructor takes the following arguments: +// +// "filename" (string): the path of a file listing the tokens. Each line may +// contain one or more whitespace separated tokens, followed by an optional +// comment starting with a "#" or "|" character. +// +// "tokens" ([]interface{}): if "filename" is not specified, tokens can be +// passed directly as a sequence of strings wrapped in a []interface{}. +package tokenmap + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/registry" +) + +const Name = "custom" + +func GenericTokenMapConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenMap, error) { + rv := analysis.NewTokenMap() + + // first: try to load by filename + filename, ok := config["filename"].(string) + if ok { + err := rv.LoadFile(filename) + return rv, err + } + // next: look for an inline word list + tokens, ok := config["tokens"].([]interface{}) + if ok { + for _, token := range tokens { + tokenStr, ok := token.(string) + if ok { + rv.AddToken(tokenStr) + } + } + return rv, nil + } + return nil, fmt.Errorf("must specify filename or list of tokens for token map") +} + +func init() { + err := registry.RegisterTokenMap(Name, GenericTokenMapConstructor) + if err != nil { + panic(err) + } +} diff --git a/analysis/tokenmap_test.go b/analysis/tokenmap_test.go new file mode 100644 index 0000000..aba2286 --- /dev/null +++ b/analysis/tokenmap_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis + +import ( + "reflect" + "testing" +) + +func TestTokenMapLoadFile(t *testing.T) { + tokenMap := NewTokenMap() + err := tokenMap.LoadFile("test_words.txt") + if err != nil { + t.Fatal(err) + } + + expectedTokens := NewTokenMap() + expectedTokens.AddToken("marty") + expectedTokens.AddToken("steve") + expectedTokens.AddToken("dustin") + expectedTokens.AddToken("siri") + expectedTokens.AddToken("multiple") + expectedTokens.AddToken("words") + expectedTokens.AddToken("with") + expectedTokens.AddToken("different") + expectedTokens.AddToken("whitespace") + + if !reflect.DeepEqual(tokenMap, expectedTokens) { + t.Errorf("expected %#v, got %#v", expectedTokens, tokenMap) + } +} diff --git a/analysis/type.go b/analysis/type.go new file mode 100644 index 0000000..f819984 --- /dev/null +++ b/analysis/type.go @@ -0,0 +1,120 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis + +import ( + "fmt" + "time" +) + +type CharFilter interface { + Filter([]byte) []byte +} + +type TokenType int + +const ( + AlphaNumeric TokenType = iota + Ideographic + Numeric + DateTime + Shingle + Single + Double + Boolean + IP +) + +// Token represents one occurrence of a term at a particular location in a +// field. +type Token struct { + // Start specifies the byte offset of the beginning of the term in the + // field. + Start int `json:"start"` + + // End specifies the byte offset of the end of the term in the field. + End int `json:"end"` + Term []byte `json:"term"` + + // Position specifies the 1-based index of the token in the sequence of + // occurrences of its term in the field. + Position int `json:"position"` + Type TokenType `json:"type"` + KeyWord bool `json:"keyword"` +} + +func (t *Token) String() string { + return fmt.Sprintf("Start: %d End: %d Position: %d Token: %s Type: %d", t.Start, t.End, t.Position, string(t.Term), t.Type) +} + +type TokenStream []*Token + +// A Tokenizer splits an input string into tokens, the usual behaviour being to +// map words to tokens. +type Tokenizer interface { + Tokenize([]byte) TokenStream +} + +// A TokenFilter adds, transforms or removes tokens from a token stream. +type TokenFilter interface { + Filter(TokenStream) TokenStream +} + +type Analyzer interface { + Analyze([]byte) TokenStream +} + +type DefaultAnalyzer struct { + CharFilters []CharFilter + Tokenizer Tokenizer + TokenFilters []TokenFilter +} + +func (a *DefaultAnalyzer) Analyze(input []byte) TokenStream { + if a.CharFilters != nil { + for _, cf := range a.CharFilters { + input = cf.Filter(input) + } + } + tokens := a.Tokenizer.Tokenize(input) + if a.TokenFilters != nil { + for _, tf := range a.TokenFilters { + tokens = tf.Filter(tokens) + } + } + return tokens +} + +var ErrInvalidDateTime = fmt.Errorf("unable to parse datetime with any of the layouts") + +var ErrInvalidTimestampString = fmt.Errorf("unable to parse timestamp string") +var ErrInvalidTimestampRange = fmt.Errorf("timestamp out of range") + +type DateTimeParser interface { + ParseDateTime(string) (time.Time, string, error) +} + +const SynonymSourceType = "synonym" + +type SynonymSourceVisitor func(name string, item SynonymSource) error + +type SynonymSource interface { + Analyzer() string + Collection() string +} + +type ByteArrayConverter interface { + Convert([]byte) (interface{}, error) +} diff --git a/analysis/util.go b/analysis/util.go new file mode 100644 index 0000000..8e4348a --- /dev/null +++ b/analysis/util.go @@ -0,0 +1,92 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis + +import ( + "bytes" + "unicode/utf8" +) + +func DeleteRune(in []rune, pos int) []rune { + if pos >= len(in) { + return in + } + copy(in[pos:], in[pos+1:]) + return in[:len(in)-1] +} + +func InsertRune(in []rune, pos int, r rune) []rune { + // create a new slice 1 rune larger + rv := make([]rune, len(in)+1) + // copy the characters before the insert pos + copy(rv[0:pos], in[0:pos]) + // set the inserted rune + rv[pos] = r + // copy the characters after the insert pos + copy(rv[pos+1:], in[pos:]) + return rv +} + +// BuildTermFromRunesOptimistic will build a term from the provided runes +// AND optimistically attempt to encode into the provided buffer +// if at any point it appears the buffer is too small, a new buffer is +// allocated and that is used instead +// this should be used in cases where frequently the new term is the same +// length or shorter than the original term (in number of bytes) +func BuildTermFromRunesOptimistic(buf []byte, runes []rune) []byte { + rv := buf + used := 0 + for _, r := range runes { + nextLen := utf8.RuneLen(r) + if used+nextLen > len(rv) { + // alloc new buf + buf = make([]byte, len(runes)*utf8.UTFMax) + // copy work we've already done + copy(buf, rv[:used]) + rv = buf + } + written := utf8.EncodeRune(rv[used:], r) + used += written + } + return rv[:used] +} + +func BuildTermFromRunes(runes []rune) []byte { + return BuildTermFromRunesOptimistic(make([]byte, len(runes)*utf8.UTFMax), runes) +} + +func TruncateRunes(input []byte, num int) []byte { + runes := bytes.Runes(input) + runes = runes[:len(runes)-num] + out := BuildTermFromRunes(runes) + return out +} + +func RunesEndsWith(input []rune, suffix string) bool { + inputLen := len(input) + suffixRunes := []rune(suffix) + suffixLen := len(suffixRunes) + if suffixLen > inputLen { + return false + } + + for i := suffixLen - 1; i >= 0; i-- { + if input[inputLen-(suffixLen-i)] != suffixRunes[i] { + return false + } + } + + return true +} diff --git a/analysis/util_test.go b/analysis/util_test.go new file mode 100644 index 0000000..ec4f89b --- /dev/null +++ b/analysis/util_test.go @@ -0,0 +1,140 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analysis + +import ( + "reflect" + "testing" +) + +func TestDeleteRune(t *testing.T) { + tests := []struct { + in []rune + delPos int + out []rune + }{ + { + in: []rune{'a', 'b', 'c'}, + delPos: 1, + out: []rune{'a', 'c'}, + }, + } + + for _, test := range tests { + actual := DeleteRune(test.in, test.delPos) + if !reflect.DeepEqual(actual, test.out) { + t.Errorf("expected %#v, got %#v", test.out, actual) + } + } +} + +func TestInsertRune(t *testing.T) { + tests := []struct { + in []rune + insPos int + insRune rune + out []rune + }{ + { + in: []rune{'a', 'b', 'c'}, + insPos: 1, + insRune: 'x', + out: []rune{'a', 'x', 'b', 'c'}, + }, + { + in: []rune{'a', 'b', 'c'}, + insPos: 0, + insRune: 'x', + out: []rune{'x', 'a', 'b', 'c'}, + }, + { + in: []rune{'a', 'b', 'c'}, + insPos: 3, + insRune: 'x', + out: []rune{'a', 'b', 'c', 'x'}, + }, + } + + for _, test := range tests { + actual := InsertRune(test.in, test.insPos, test.insRune) + if !reflect.DeepEqual(actual, test.out) { + t.Errorf("expected %#v, got %#v", test.out, actual) + } + } +} + +func TestBuildTermFromRunes(t *testing.T) { + tests := []struct { + in []rune + }{ + { + in: []rune{'a', 'b', 'c'}, + }, + { + in: []rune{'こ', 'ん', 'に', 'ち', 'は', '世', '界'}, + }, + } + for _, test := range tests { + out := BuildTermFromRunes(test.in) + back := []rune(string(out)) + if !reflect.DeepEqual(back, test.in) { + t.Errorf("expected %v to convert back to %v", out, test.in) + } + } +} + +func TestBuildTermFromRunesOptimistic(t *testing.T) { + tests := []struct { + buf []byte + in []rune + }{ + { + buf: []byte("abc"), + in: []rune{'a', 'b', 'c'}, + }, + { + buf: []byte("こんにちは世界"), + in: []rune{'こ', 'ん', 'に', 'ち', 'は', '世', '界'}, + }, + // same, but don't give enough buffer + { + buf: []byte("ab"), + in: []rune{'a', 'b', 'c'}, + }, + { + buf: []byte("こ"), + in: []rune{'こ', 'ん', 'に', 'ち', 'は', '世', '界'}, + }, + } + for _, test := range tests { + out := BuildTermFromRunesOptimistic(test.buf, test.in) + back := []rune(string(out)) + if !reflect.DeepEqual(back, test.in) { + t.Errorf("expected %v to convert back to %v", out, test.in) + } + } +} + +func BenchmarkBuildTermFromRunes(b *testing.B) { + input := [][]rune{ + {'a', 'b', 'c'}, + {'こ', 'ん', 'に', 'ち', 'は', '世', '界'}, + } + for i := 0; i < b.N; i++ { + for _, i := range input { + BuildTermFromRunes(i) + } + } +} diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..30285a2 --- /dev/null +++ b/builder.go @@ -0,0 +1,94 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type builderImpl struct { + b index.IndexBuilder + m mapping.IndexMapping +} + +func (b *builderImpl) Index(id string, data interface{}) error { + if id == "" { + return ErrorEmptyID + } + + doc := document.NewDocument(id) + err := b.m.MapDocument(doc, data) + if err != nil { + return err + } + err = b.b.Index(doc) + return err +} + +func (b *builderImpl) Close() error { + return b.b.Close() +} + +func newBuilder(path string, mapping mapping.IndexMapping, config map[string]interface{}) (Builder, error) { + if path == "" { + return nil, fmt.Errorf("builder requires path") + } + + err := mapping.Validate() + if err != nil { + return nil, err + } + + if config == nil { + config = map[string]interface{}{} + } + + // the builder does not have an API to interact with internal storage + // however we can pass k/v pairs through the config + mappingBytes, err := util.MarshalJSON(mapping) + if err != nil { + return nil, err + } + config["internal"] = map[string][]byte{ + string(mappingInternalKey): mappingBytes, + } + + // do not use real config, as these are options for the builder, + // not the resulting index + meta := newIndexMeta(scorch.Name, scorch.Name, map[string]interface{}{}) + err = meta.Save(path) + if err != nil { + return nil, err + } + + config["path"] = indexStorePath(path) + + b, err := scorch.NewBuilder(config) + if err != nil { + return nil, err + } + rv := &builderImpl{ + b: b, + m: mapping, + } + + return rv, nil +} diff --git a/builder_test.go b/builder_test.go new file mode 100644 index 0000000..cd9de3e --- /dev/null +++ b/builder_test.go @@ -0,0 +1,88 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "fmt" + "os" + "testing" +) + +func TestBuilder(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "bleve-scorch-builder-test") + if err != nil { + t.Fatal(err) + } + defer func() { + err = os.RemoveAll(tmpDir) + if err != nil { + t.Fatalf("error cleaning up test index") + } + }() + + conf := map[string]interface{}{ + "batchSize": 2, + "mergeMax": 2, + } + b, err := NewBuilder(tmpDir, NewIndexMapping(), conf) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 10; i++ { + doc := map[string]interface{}{ + "name": "hello", + } + err = b.Index(fmt.Sprintf("%d", i), doc) + if err != nil { + t.Fatal(err) + } + } + + err = b.Close() + if err != nil { + t.Fatal(err) + } + + idx, err := Open(tmpDir) + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Errorf("error closing index: %v", err) + } + }() + + docCount, err := idx.DocCount() + if err != nil { + t.Errorf("error checking doc count: %v", err) + } + if docCount != 10 { + t.Errorf("expected doc count to be 10, got %d", docCount) + } + + q := NewTermQuery("hello") + q.SetField("name") + req := NewSearchRequest(q) + res, err := idx.Search(req) + if err != nil { + t.Errorf("error searching index: %v", err) + } + if res.Total != 10 { + t.Errorf("expected 10 search hits, got %d", res.Total) + } +} diff --git a/cmd/bleve/.gitignore b/cmd/bleve/.gitignore new file mode 100644 index 0000000..3a664b8 --- /dev/null +++ b/cmd/bleve/.gitignore @@ -0,0 +1,2 @@ +/vendor/github.com/spf13/cobra/cobra +/vendor/github.com/spf13/cobra/doc diff --git a/cmd/bleve/cmd/bulk.go b/cmd/bleve/cmd/bulk.go new file mode 100644 index 0000000..458c3c7 --- /dev/null +++ b/cmd/bleve/cmd/bulk.go @@ -0,0 +1,116 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bufio" + "encoding/json" + "fmt" + "math/rand" + "os" + + "github.com/spf13/cobra" +) + +var batchSize int + +// bulkCmd represents the bulk command +var bulkCmd = &cobra.Command{ + Use: "bulk [index path] [data paths ...]", + Short: "bulk loads from newline delimited JSON files", + Long: `The bulk command will perform batch loading of documents in one or more newline delimited JSON files.`, + Annotations: map[string]string{ + canMutateBleveIndex: "true", + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("must specify at least one path") + } + + i := 0 + batch := idx.NewBatch() + + for _, file := range args[1:] { + + file, err := os.Open(file) + if err != nil { + return err + } + + fmt.Printf("Indexing: %s\n", file.Name()) + r := bufio.NewReader(file) + + for { + if i%batchSize == 0 { + fmt.Printf("Indexing batch (%d docs)...\n", i) + err := idx.Batch(batch) + if err != nil { + return err + } + batch = idx.NewBatch() + } + + b, _ := r.ReadBytes('\n') + if len(b) == 0 { + break + } + + var doc interface{} = b + var err error + if parseJSON { + err = json.Unmarshal(b, &doc) + if err != nil { + return fmt.Errorf("error parsing JSON: %v", err) + } + } + + docID := randomString(5) + err = batch.Index(docID, doc) + if err != nil { + return err + } + i++ + } + err = idx.Batch(batch) + if err != nil { + return err + } + + err = file.Close() + if err != nil { + return err + } + + } + return nil + }, +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func randomString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +func init() { + RootCmd.AddCommand(bulkCmd) + + bulkCmd.Flags().IntVarP(&batchSize, "batch", "b", 1000, "Batch size for loading.") + bulkCmd.Flags().BoolVarP(&parseJSON, "json", "j", true, "Parse the contents as JSON.") +} diff --git a/cmd/bleve/cmd/check.go b/cmd/bleve/cmd/check.go new file mode 100644 index 0000000..ca4f286 --- /dev/null +++ b/cmd/bleve/cmd/check.go @@ -0,0 +1,131 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "log" + + "github.com/blevesearch/bleve/v2" + "github.com/spf13/cobra" +) + +var checkFieldName string +var checkCount int + +// checkCmd represents the check command +var checkCmd = &cobra.Command{ + Use: "check [index path]", + Short: "checks the contents of the index", + Long: `The check command will perform consistency checks on the index.`, + RunE: func(cmd *cobra.Command, args []string) error { + + var fieldNames []string + var err error + if checkFieldName == "" { + fieldNames, err = idx.Fields() + if err != nil { + return err + } + } else { + fieldNames = []string{checkFieldName} + } + fmt.Printf("checking fields: %v\n", fieldNames) + + totalProblems := 0 + for _, fieldName := range fieldNames { + fmt.Printf("checking field: '%s'\n", fieldName) + problems, err := checkField(idx, fieldName) + if err != nil { + log.Fatal(err) + } + totalProblems += problems + } + + if totalProblems != 0 { + return fmt.Errorf("found %d total problems\n", totalProblems) + } + + return nil + }, +} + +func checkField(index bleve.Index, fieldName string) (int, error) { + termDictionary, err := getDictionary(index, fieldName) + if err != nil { + return 0, err + } + fmt.Printf("field contains %d terms\n", len(termDictionary)) + + numTested := 0 + numProblems := 0 + for term, count := range termDictionary { + fmt.Printf("checked %d terms\r", numTested) + if checkCount > 0 && numTested >= checkCount { + break + } + + tq := bleve.NewTermQuery(term) + tq.SetField(fieldName) + req := bleve.NewSearchRequest(tq) + req.Size = 0 + res, err := index.Search(req) + if err != nil { + return 0, err + } + + if res.Total != count { + fmt.Printf("unexpected mismatch for term '%s', dictionary %d, search hits %d\n", term, count, res.Total) + numProblems++ + } + + numTested++ + } + fmt.Printf("done checking %d terms, found %d problems\n", numTested, numProblems) + + return numProblems, nil +} + +func getDictionary(index bleve.Index, field string) (map[string]uint64, error) { + rv := make(map[string]uint64) + i, err := index.Advanced() + if err != nil { + log.Fatal(err) + } + r, err := i.Reader() + if err != nil { + log.Fatal(err) + } + d, err := r.FieldDict(field) + if err != nil { + log.Fatal(err) + } + + de, err := d.Next() + for err == nil && de != nil { + rv[de.Term] = de.Count + de, err = d.Next() + } + if err != nil { + return nil, err + } + return rv, nil +} + +func init() { + RootCmd.AddCommand(checkCmd) + checkCmd.Flags().StringVarP(&checkFieldName, "field", "f", "", "Restrict check to the specified field name.") + checkCmd.Flags().IntVarP(&checkCount, "count", "c", 100, "Check this many terms.") +} diff --git a/cmd/bleve/cmd/count.go b/cmd/bleve/cmd/count.go new file mode 100644 index 0000000..15ef4d6 --- /dev/null +++ b/cmd/bleve/cmd/count.go @@ -0,0 +1,40 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// countCmd represents the count command +var countCmd = &cobra.Command{ + Use: "count [index path]", + Short: "counts the number documents in the index", + Long: `The count command will count the number of documents in the index.`, + RunE: func(cmd *cobra.Command, args []string) error { + count, err := idx.DocCount() + if err != nil { + return fmt.Errorf("error counting docs in index: %v", err) + } + fmt.Printf("%d\n", count) + return nil + }, +} + +func init() { + RootCmd.AddCommand(countCmd) +} diff --git a/cmd/bleve/cmd/create.go b/cmd/bleve/cmd/create.go new file mode 100644 index 0000000..a9ea899 --- /dev/null +++ b/cmd/bleve/cmd/create.go @@ -0,0 +1,81 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/spf13/cobra" +) + +var mappingPath, indexType, storeType string + +// createCmd represents the create command +var createCmd = &cobra.Command{ + Use: "create [index path]", + Short: "creates a new index", + Long: `The create command will create a new empty index.`, + Annotations: map[string]string{ + canMutateBleveIndex: "true", + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // override RootCmd version which opens existing index + if len(args) < 1 { + return fmt.Errorf("must specify path to index") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var mapping mapping.IndexMapping + var err error + mapping, err = buildMapping() + if err != nil { + return fmt.Errorf("error building mapping: %v", err) + } + idx, err = bleve.NewUsing(args[0], mapping, indexType, storeType, nil) + if err != nil { + return fmt.Errorf("error creating index: %v", err) + } + // the inheritted Post action will close the index + return nil + }, +} + +func buildMapping() (mapping.IndexMapping, error) { + mapping := mapping.NewIndexMapping() + if mappingPath != "" { + mappingBytes, err := os.ReadFile(mappingPath) + if err != nil { + return nil, err + } + err = json.Unmarshal(mappingBytes, &mapping) + if err != nil { + return nil, err + } + } + return mapping, nil +} + +func init() { + RootCmd.AddCommand(createCmd) + + createCmd.Flags().StringVarP(&mappingPath, "mapping", "m", "", "Path to a file containing a JSON representation of an index mapping to use.") + createCmd.Flags().StringVarP(&storeType, "store", "s", bleve.Config.DefaultKVStore, "The bleve storage type to use.") + createCmd.Flags().StringVarP(&indexType, "index", "i", bleve.Config.DefaultIndexType, "The bleve index type to use.") +} diff --git a/cmd/bleve/cmd/dictionary.go b/cmd/bleve/cmd/dictionary.go new file mode 100644 index 0000000..3f42d12 --- /dev/null +++ b/cmd/bleve/cmd/dictionary.go @@ -0,0 +1,60 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// dictionaryCmd represents the dictionary command +var dictionaryCmd = &cobra.Command{ + Use: "dictionary [index path] [field name]", + Short: "prints the term dictionary for the specified field in the index", + Long: `The dictionary command will print the term dictionary for the specified field.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("must specify field") + } + i, err := idx.Advanced() + if err != nil { + return fmt.Errorf("error getting index: %v", err) + } + r, err := i.Reader() + if err != nil { + return fmt.Errorf("error getting index reader: %v", err) + } + d, err := r.FieldDict(args[1]) + if err != nil { + return fmt.Errorf("error getting field dictionary: %v", err) + } + + de, err := d.Next() + for err == nil && de != nil { + fmt.Printf("%s - %d\n", de.Term, de.Count) + de, err = d.Next() + } + if err != nil { + return fmt.Errorf("error iterating dictionary: %v", err) + } + + return nil + }, +} + +func init() { + RootCmd.AddCommand(dictionaryCmd) +} diff --git a/cmd/bleve/cmd/dump.go b/cmd/bleve/cmd/dump.go new file mode 100644 index 0000000..db76eeb --- /dev/null +++ b/cmd/bleve/cmd/dump.go @@ -0,0 +1,61 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/spf13/cobra" +) + +var docID string + +// dumpCmd represents the dump command +var dumpCmd = &cobra.Command{ + Use: "dump [index path]", + Short: "dumps the contents of the index", + Long: `The dump command will dump (possibly a section of) the index.`, + RunE: func(cmd *cobra.Command, args []string) error { + i, err := idx.Advanced() + if err != nil { + return fmt.Errorf("error getting index: %v", err) + } + r, err := i.Reader() + if err != nil { + return fmt.Errorf("error getting index reader: %v", err) + } + upsideDownReader, ok := r.(*upsidedown.IndexReader) + if !ok { + return fmt.Errorf("dump is only supported by index type upsidedown") + } + + dumpChan := upsideDownReader.DumpAll() + for rowOrErr := range dumpChan { + switch rowOrErr := rowOrErr.(type) { + case error: + return fmt.Errorf("error dumping: %v", rowOrErr) + case upsidedown.UpsideDownCouchRow: + fmt.Printf("%v\n", rowOrErr) + fmt.Printf("Key: % -100x\nValue: % -100x\n\n", rowOrErr.Key(), rowOrErr.Value()) + } + } + return nil + }, +} + +func init() { + RootCmd.AddCommand(dumpCmd) +} diff --git a/cmd/bleve/cmd/dumpDoc.go b/cmd/bleve/cmd/dumpDoc.go new file mode 100644 index 0000000..4473ab6 --- /dev/null +++ b/cmd/bleve/cmd/dumpDoc.go @@ -0,0 +1,63 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/spf13/cobra" +) + +// dumpDocCmd represents the dumpDoc command +var dumpDocCmd = &cobra.Command{ + Use: "doc [index path] [doc id]", + Short: "dump only the rows relating to this doc ID", + Long: `The doc sub-command of dump will only dump the rows relating to this doc ID.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("must specify docid") + } + + i, err := idx.Advanced() + if err != nil { + return fmt.Errorf("error getting index: %v", err) + } + r, err := i.Reader() + if err != nil { + return fmt.Errorf("error getting index reader: %v", err) + } + upsideDownReader, ok := r.(*upsidedown.IndexReader) + if !ok { + return fmt.Errorf("dump doc is only supported by index type upsidedown") + } + + dumpChan := upsideDownReader.DumpDoc(args[1]) + for rowOrErr := range dumpChan { + switch rowOrErr := rowOrErr.(type) { + case error: + return fmt.Errorf("error dumping: %v", rowOrErr) + case upsidedown.UpsideDownCouchRow: + fmt.Printf("%v\n", rowOrErr) + fmt.Printf("Key: % -100x\nValue: % -100x\n\n", rowOrErr.Key(), rowOrErr.Value()) + } + } + return nil + }, +} + +func init() { + dumpCmd.AddCommand(dumpDocCmd) +} diff --git a/cmd/bleve/cmd/dumpFields.go b/cmd/bleve/cmd/dumpFields.go new file mode 100644 index 0000000..0021ff9 --- /dev/null +++ b/cmd/bleve/cmd/dumpFields.go @@ -0,0 +1,59 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/spf13/cobra" +) + +// dumpFieldsCmd represents the dumpFields command +var dumpFieldsCmd = &cobra.Command{ + Use: "fields [index path]", + Short: "dump only the field rows", + Long: `The fields sub-command of dump will only dump the field rows.`, + RunE: func(cmd *cobra.Command, args []string) error { + i, err := idx.Advanced() + if err != nil { + return fmt.Errorf("error getting index: %v", err) + } + r, err := i.Reader() + if err != nil { + return fmt.Errorf("error getting index reader: %v", err) + } + upsideDownReader, ok := r.(*upsidedown.IndexReader) + if !ok { + return fmt.Errorf("dump fields is only supported by index type upsidedown") + } + + dumpChan := upsideDownReader.DumpFields() + for rowOrErr := range dumpChan { + switch rowOrErr := rowOrErr.(type) { + case error: + return fmt.Errorf("error dumping: %v", rowOrErr) + case upsidedown.UpsideDownCouchRow: + fmt.Printf("%v\n", rowOrErr) + fmt.Printf("Key: % -100x\nValue: % -100x\n\n", rowOrErr.Key(), rowOrErr.Value()) + } + } + return nil + }, +} + +func init() { + dumpCmd.AddCommand(dumpFieldsCmd) +} diff --git a/cmd/bleve/cmd/fields.go b/cmd/bleve/cmd/fields.go new file mode 100644 index 0000000..11b369f --- /dev/null +++ b/cmd/bleve/cmd/fields.go @@ -0,0 +1,50 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// fieldsCmd represents the fields command +var fieldsCmd = &cobra.Command{ + Use: "fields [index path]", + Short: "lists the fields in this index", + Long: `The fields command will list the fields used in this index.`, + RunE: func(cmd *cobra.Command, args []string) error { + i, err := idx.Advanced() + if err != nil { + return fmt.Errorf("error getting index: %v", err) + } + r, err := i.Reader() + if err != nil { + return fmt.Errorf("error getting index reader: %v", err) + } + fields, err := r.Fields() + if err != nil { + return fmt.Errorf("error getting fields: %v", err) + } + for i, field := range fields { + fmt.Printf("%d - %s\n", i, field) + } + return nil + }, +} + +func init() { + RootCmd.AddCommand(fieldsCmd) +} diff --git a/cmd/bleve/cmd/index.go b/cmd/bleve/cmd/index.go new file mode 100644 index 0000000..bfc27fc --- /dev/null +++ b/cmd/bleve/cmd/index.go @@ -0,0 +1,116 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +var keepDir, keepExt, parseJSON bool + +// indexCmd represents the index command +var indexCmd = &cobra.Command{ + Use: "index [index path] [data paths ...]", + Short: "adds the files to the index", + Long: `The index command adds the specified files to the index.`, + Annotations: map[string]string{ + canMutateBleveIndex: "true", + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("must specify at least one path") + } + for file := range handleArgs(args[1:]) { + var doc interface{} + // index the files + docID := file.filename + if !keepDir { + _, docID = filepath.Split(docID) + } + if !keepExt { + ext := filepath.Ext(docID) + docID = docID[0 : len(docID)-len(ext)] + } + doc = file.contents + var err error + if parseJSON { + err = json.Unmarshal(file.contents, &doc) + if err != nil { + return fmt.Errorf("error parsing JSON: %v", err) + } + } + fmt.Printf("Indexing: %s\n", docID) + err = idx.Index(docID, doc) + if err != nil { + return fmt.Errorf("error indexing: %v", err) + } + } + return nil + }, +} + +type file struct { + filename string + contents []byte +} + +func handleArgs(args []string) chan file { + rv := make(chan file) + go getAllFiles(args, rv) + return rv +} + +func getAllFiles(args []string, rv chan file) { + for _, arg := range args { + arg = filepath.Clean(arg) + err := filepath.Walk(arg, func(path string, finfo os.FileInfo, err error) error { + if err != nil { + log.Print(err) + return err + } + if finfo.IsDir() { + return nil + } + + bytes, err := os.ReadFile(path) + if err != nil { + log.Fatal(err) + } + rv <- file{ + filename: filepath.Base(path), + contents: bytes, + } + return nil + }) + if err != nil { + log.Fatal(err) + } + } + close(rv) +} + +func init() { + RootCmd.AddCommand(indexCmd) + + indexCmd.Flags().BoolVarP(&keepDir, "keepDir", "d", false, "Keep the directory in the document id.") + indexCmd.Flags().BoolVarP(&keepExt, "keepExt", "x", false, "Keep the extension in the document id.") + indexCmd.Flags().BoolVarP(&parseJSON, "json", "j", true, "Parse the contents as JSON.") +} diff --git a/cmd/bleve/cmd/mapping.go b/cmd/bleve/cmd/mapping.go new file mode 100644 index 0000000..1df471d --- /dev/null +++ b/cmd/bleve/cmd/mapping.go @@ -0,0 +1,42 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/spf13/cobra" +) + +// mappingCmd represents the mapping command +var mappingCmd = &cobra.Command{ + Use: "mapping [index path]", + Short: "prints the mapping used for this index", + Long: `The mapping command prints a JSON representation of the mapping used for this index.`, + Run: func(cmd *cobra.Command, args []string) { + mapping := idx.Mapping() + jsonBytes, err := json.MarshalIndent(mapping, "", " ") + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", jsonBytes) + }, +} + +func init() { + RootCmd.AddCommand(mappingCmd) +} diff --git a/cmd/bleve/cmd/query.go b/cmd/bleve/cmd/query.go new file mode 100644 index 0000000..e1b4bec --- /dev/null +++ b/cmd/bleve/cmd/query.go @@ -0,0 +1,101 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "strings" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/search/query" + "github.com/spf13/cobra" +) + +var limit, skip, repeat int +var explain, highlight, fields bool +var qtype, qfield, sortby string + +// queryCmd represents the query command +var queryCmd = &cobra.Command{ + Use: "query [index path] [query]", + Short: "queries the index", + Long: `The query command will execute a query against the index.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("must specify query") + } + + query := buildQuery(args) + for i := 0; i < repeat; i++ { + req := bleve.NewSearchRequestOptions(query, limit, skip, explain) + if highlight { + req.Highlight = bleve.NewHighlightWithStyle("ansi") + } + if fields { + req.Fields = []string{"*"} + } + if sortby != "" { + if strings.Contains(sortby, ",") { + req.SortBy(strings.Split(sortby, ",")) + } else { + req.SortBy([]string{sortby}) + } + } + res, err := idx.Search(req) + if err != nil { + return fmt.Errorf("error running query: %v", err) + } + fmt.Println(res) + } + return nil + }, +} + +func buildQuery(args []string) query.Query { + var q query.Query + switch qtype { + case "prefix": + pquery := bleve.NewPrefixQuery(strings.Join(args[1:], " ")) + if qfield != "" { + pquery.SetField(qfield) + } + q = pquery + case "term": + pquery := bleve.NewTermQuery(strings.Join(args[1:], " ")) + if qfield != "" { + pquery.SetField(qfield) + } + q = pquery + default: + // build a search with the provided parameters + queryString := strings.Join(args[1:], " ") + q = bleve.NewQueryStringQuery(queryString) + } + return q +} + +func init() { + RootCmd.AddCommand(queryCmd) + + queryCmd.Flags().IntVarP(&repeat, "repeat", "r", 1, "Repeat the query this many times.") + queryCmd.Flags().IntVarP(&limit, "limit", "l", 10, "Limit number of results returned.") + queryCmd.Flags().IntVarP(&skip, "skip", "s", 0, "Skip the first N results.") + queryCmd.Flags().BoolVarP(&explain, "explain", "x", false, "Explain the result scoring.") + queryCmd.Flags().BoolVar(&highlight, "highlight", true, "Highlight matching text in results.") + queryCmd.Flags().BoolVar(&fields, "fields", false, "Load stored fields.") + queryCmd.Flags().StringVarP(&qtype, "type", "t", "query_string", "Type of query to run.") + queryCmd.Flags().StringVarP(&qfield, "field", "f", "", "Restrict query to field, not applicable to query_string queries.") + queryCmd.Flags().StringVarP(&sortby, "sort-by", "b", "", "Sort by field.") +} diff --git a/cmd/bleve/cmd/registry.go b/cmd/bleve/cmd/registry.go new file mode 100644 index 0000000..9d5fc3f --- /dev/null +++ b/cmd/bleve/cmd/registry.go @@ -0,0 +1,88 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "sort" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/spf13/cobra" +) + +// registryCmd represents the registry command +var registryCmd = &cobra.Command{ + Use: "registry", + Short: "registry lists the bleve components compiled into this executable", + Long: `The registry command will list all of the bleve components compiled into this executable.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // override to do nothing + return nil + }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + // override to do nothing + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + types, instances := registry.CharFilterTypesAndInstances() + printType("Char Filter", types, instances) + + types, instances = registry.TokenizerTypesAndInstances() + printType("Tokenizer", types, instances) + + types, instances = registry.TokenMapTypesAndInstances() + printType("Token Map", types, instances) + + types, instances = registry.TokenFilterTypesAndInstances() + printType("Token Filter", types, instances) + + types, instances = registry.AnalyzerTypesAndInstances() + printType("Analyzer", types, instances) + + types, instances = registry.DateTimeParserTypesAndInstances() + printType("Date Time Parser", types, instances) + + types, instances = registry.KVStoreTypesAndInstances() + printType("KV Store", types, instances) + + types, instances = registry.FragmentFormatterTypesAndInstances() + printType("Fragment Formatter", types, instances) + + types, instances = registry.FragmenterTypesAndInstances() + printType("Fragmenter", types, instances) + + types, instances = registry.HighlighterTypesAndInstances() + printType("Highlighter", types, instances) + }, +} + +func printType(label string, types, instances []string) { + sort.Strings(types) + sort.Strings(instances) + fmt.Printf(label + " Types:\n") + for _, name := range types { + fmt.Printf("\t%s\n", name) + } + fmt.Println() + fmt.Printf(label + " Instances:\n") + for _, name := range instances { + fmt.Printf("\t%s\n", name) + } + fmt.Println() +} + +func init() { + RootCmd.AddCommand(registryCmd) +} diff --git a/cmd/bleve/cmd/root.go b/cmd/bleve/cmd/root.go new file mode 100644 index 0000000..7dfa465 --- /dev/null +++ b/cmd/bleve/cmd/root.go @@ -0,0 +1,93 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + "strconv" + + "github.com/blevesearch/bleve/v2" + "github.com/spf13/cobra" +) + +var cfgFile string + +var idx bleve.Index + +// DefaultOpenReadOnly allows some distributions of this command to default +// to always opening the index read-only +var DefaultOpenReadOnly = false + +const canMutateBleveIndex = "canMutateBleveIndex" + +// CanMutateBleveIndex returns true if the command is capable +// of mutating the bleve index, or false if its operation is +// read-only +func CanMutateBleveIndex(c *cobra.Command) bool { + for k, v := range c.Annotations { + if k == canMutateBleveIndex { + if b, err := strconv.ParseBool(v); err == nil && b { + return true + } + } + } + return false +} + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "bleve", + Short: "command-line tool to interact with a bleve index", + Long: `Bleve is a command-line tool to interact with a bleve index.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if cmd.Use == "bash" || cmd.Use == "zsh" || cmd.Use == "fish" || cmd.Use == "powershell" { + // Not applicable to cobra's completion subcommands + return nil + } + if len(args) < 1 { + return fmt.Errorf("must specify path to index") + } + runtimeConfig := map[string]interface{}{ + "read_only": DefaultOpenReadOnly, + } + var err error + idx, err = bleve.OpenUsing(args[0], runtimeConfig) + if err != nil { + return fmt.Errorf("error opening bleve index: %v", err) + } + return nil + }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + if cmd.Use == "bash" || cmd.Use == "zsh" || cmd.Use == "fish" || cmd.Use == "powershell" { + // Not applicable to cobra's completion subcommands + return nil + } + err := idx.Close() + if err != nil { + return fmt.Errorf("error closing bleve index: %v", err) + } + return nil + }, +} + +// Execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} diff --git a/cmd/bleve/cmd/scorch.go b/cmd/bleve/cmd/scorch.go new file mode 100644 index 0000000..d899151 --- /dev/null +++ b/cmd/bleve/cmd/scorch.go @@ -0,0 +1,25 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/blevesearch/bleve/v2/cmd/bleve/cmd/scorch" +) + +// make scorch command-line tool a bleve sub-command + +func init() { + RootCmd.AddCommand(scorch.RootCmd) +} diff --git a/cmd/bleve/cmd/scorch/ascii.go b/cmd/bleve/cmd/scorch/ascii.go new file mode 100644 index 0000000..f6bdaec --- /dev/null +++ b/cmd/bleve/cmd/scorch/ascii.go @@ -0,0 +1,59 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "strconv" + + "github.com/blevesearch/bleve/v2/index/scorch/mergeplan" + "github.com/spf13/cobra" +) + +// asciiCmd represents the ascii command +var asciiCmd = &cobra.Command{ + Use: "ascii", + Short: "ascii prints an ascii representation of the segments in a snapshot", + Long: `The ascii command prints an ascii representation of the segments in a given snapshot.`, + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) < 2 { + return fmt.Errorf("snapshot epoch required") + } else if len(args) < 3 { + snapshotEpoch, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + snapshot, err := index.LoadSnapshot(snapshotEpoch) + if err != nil { + return err + } + segments := snapshot.Segments() + var mergePlanSegments []mergeplan.Segment + for _, v := range segments { + mergePlanSegments = append(mergePlanSegments, v) + } + + str := mergeplan.ToBarChart(args[1], 25, mergePlanSegments, nil) + fmt.Printf("%s\n", str) + } + + return nil + }, +} + +func init() { + RootCmd.AddCommand(asciiCmd) +} diff --git a/cmd/bleve/cmd/scorch/deleted.go b/cmd/bleve/cmd/scorch/deleted.go new file mode 100644 index 0000000..cb2a924 --- /dev/null +++ b/cmd/bleve/cmd/scorch/deleted.go @@ -0,0 +1,55 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +// deletedCmd represents the deleted command +var deletedCmd = &cobra.Command{ + Use: "deleted", + Short: "deleted prints the deleted bitmap for segments in the index snapshot", + Long: `The delete command prints the deleted bitmap for segments in the index snapshot.`, + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) < 2 { + return fmt.Errorf("snapshot epoch required") + } else if len(args) < 3 { + snapshotEpoch, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + snapshot, err := index.LoadSnapshot(snapshotEpoch) + if err != nil { + return err + } + segments := snapshot.Segments() + for i, segmentSnap := range segments { + deleted := segmentSnap.Deleted() + fmt.Printf("%d %v\n", i, deleted) + } + } + + return nil + }, +} + +func init() { + RootCmd.AddCommand(deletedCmd) +} diff --git a/cmd/bleve/cmd/scorch/info.go b/cmd/bleve/cmd/scorch/info.go new file mode 100644 index 0000000..31e6481 --- /dev/null +++ b/cmd/bleve/cmd/scorch/info.go @@ -0,0 +1,59 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// dictCmd represents the dict command +var infoCmd = &cobra.Command{ + Use: "info", + Short: "info prints basic info about the index", + Long: `The info command prints basic info about the index.`, + RunE: func(cmd *cobra.Command, args []string) error { + + reader, err := index.Reader() + if err != nil { + return err + } + + count, err := reader.DocCount() + if err != nil { + return err + } + + fmt.Printf("doc count: %d\n", count) + + // var numSnapshots int + // var rootSnapshot uint64 + // index.VisitBoltSnapshots(func(snapshotEpoch uint64) error { + // if rootSnapshot == 0 { + // rootSnapshot = snapshotEpoch + // } + // numSnapshots++ + // return nil + // }) + // fmt.Printf("has %d snapshot(s), root: %d\n", numSnapshots, rootSnapshot) + + return nil + }, +} + +func init() { + RootCmd.AddCommand(infoCmd) +} diff --git a/cmd/bleve/cmd/scorch/internal.go b/cmd/bleve/cmd/scorch/internal.go new file mode 100644 index 0000000..dc94979 --- /dev/null +++ b/cmd/bleve/cmd/scorch/internal.go @@ -0,0 +1,61 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +var ascii bool + +// internalCmd represents the internal command +var internalCmd = &cobra.Command{ + Use: "internal", + Short: "internal prints the internal k/v pairs in a snapshot", + Long: `The internal command prints the internal k/v pairs in a snapshot.`, + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) < 2 { + return fmt.Errorf("snapshot epoch required") + } else if len(args) < 3 { + snapshotEpoch, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + snapshot, err := index.LoadSnapshot(snapshotEpoch) + if err != nil { + return err + } + internal := snapshot.Internal() + for k, v := range internal { + if ascii { + fmt.Printf("%s %s\n", k, string(v)) + } else { + fmt.Printf("%x %x\n", k, v) + } + } + } + + return nil + }, +} + +func init() { + RootCmd.AddCommand(internalCmd) + internalCmd.Flags().BoolVarP(&ascii, "ascii", "a", false, "print key/value in ascii") +} diff --git a/cmd/bleve/cmd/scorch/root.go b/cmd/bleve/cmd/scorch/root.go new file mode 100644 index 0000000..5e2a441 --- /dev/null +++ b/cmd/bleve/cmd/scorch/root.go @@ -0,0 +1,70 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "os" + + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/spf13/cobra" +) + +var index *scorch.Scorch + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "scorch", + Short: "command-line tool to interact with a scorch index", + Long: `Scorch is a command-line tool to interact with a scorch index.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + + if len(args) < 1 { + return fmt.Errorf("must specify path to scorch index") + } + + readOnly := true + config := map[string]interface{}{ + "read_only": readOnly, + "path": args[0], + } + + idx, err := scorch.NewScorch(scorch.Name, config, nil) + if err != nil { + return err + } + + err = idx.Open() + if err != nil { + return fmt.Errorf("error opening: %v", err) + } + + index = idx.(*scorch.Scorch) + + return nil + }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + return nil + }, +} + +// Execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} diff --git a/cmd/bleve/cmd/scorch/snapshot.go b/cmd/bleve/cmd/scorch/snapshot.go new file mode 100644 index 0000000..cf85398 --- /dev/null +++ b/cmd/bleve/cmd/scorch/snapshot.go @@ -0,0 +1,64 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "strconv" + + seg "github.com/blevesearch/scorch_segment_api/v2" + "github.com/spf13/cobra" +) + +// snapshotCmd represents the snapshot command +var snapshotCmd = &cobra.Command{ + Use: "snapshot", + Short: "info prints details about the snapshots in the index", + Long: `The snapshot command prints details about the snapshots in the index.`, + RunE: func(cmd *cobra.Command, args []string) error { + + if len(args) < 2 { + snapshotEpochs, err := index.RootBoltSnapshotEpochs() + if err != nil { + return err + } + for _, snapshotEpoch := range snapshotEpochs { + fmt.Printf("snapshot epoch: %d\n", snapshotEpoch) + } + } else if len(args) < 3 { + snapshotEpoch, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + snapshot, err := index.LoadSnapshot(snapshotEpoch) + if err != nil { + return err + } + segments := snapshot.Segments() + for i, segmentSnap := range segments { + segment := segmentSnap.Segment() + if segment, ok := segment.(seg.PersistedSegment); ok { + fmt.Printf("%d %s\n", i, segment.Path()) + } + } + } + + return nil + }, +} + +func init() { + RootCmd.AddCommand(snapshotCmd) +} diff --git a/cmd/bleve/gendocs.go b/cmd/bleve/gendocs.go new file mode 100644 index 0000000..3ff9b8d --- /dev/null +++ b/cmd/bleve/gendocs.go @@ -0,0 +1,45 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build ignore +// +build ignore + +package main + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/cmd/bleve/cmd" + + "github.com/spf13/cobra/doc" +) + +// you can generate markdown docs by running +// +// $ go run gendocs.go +// +// this also requires doc sub-package of cobra +// which is not kept in this repo +// you can acquire it by running +// +// $ gvt restore + +func main() { + cmd.RootCmd.DisableAutoGenTag = true + identity := func(s string) string { + return fmt.Sprintf(`{{< relref "docs/%s" >}}`, s) + } + emptyStr := func(s string) string { return "" } + doc.GenMarkdownTreeCustom(cmd.RootCmd, "./", emptyStr, identity) +} diff --git a/cmd/bleve/main.go b/cmd/bleve/main.go new file mode 100644 index 0000000..64e6279 --- /dev/null +++ b/cmd/bleve/main.go @@ -0,0 +1,26 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/blevesearch/bleve/v2/cmd/bleve/cmd" + + // to support standard set of build tags + _ "github.com/blevesearch/bleve/v2/config" +) + +func main() { + cmd.Execute() +} diff --git a/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/LICENSE b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/LICENSE new file mode 100644 index 0000000..5f0d1fb --- /dev/null +++ b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/LICENSE @@ -0,0 +1,13 @@ +Copyright 2014 Alan Shreve + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_others.go b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_others.go new file mode 100644 index 0000000..9d2d8a4 --- /dev/null +++ b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_others.go @@ -0,0 +1,15 @@ +// +build !windows + +package mousetrap + +// StartedByExplorer returns true if the program was invoked by the user +// double-clicking on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +// +// On non-Windows platforms, it always returns false. +func StartedByExplorer() bool { + return false +} diff --git a/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows.go b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows.go new file mode 100644 index 0000000..336142a --- /dev/null +++ b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows.go @@ -0,0 +1,98 @@ +// +build windows +// +build !go1.4 + +package mousetrap + +import ( + "fmt" + "os" + "syscall" + "unsafe" +) + +const ( + // defined by the Win32 API + th32cs_snapprocess uintptr = 0x2 +) + +var ( + kernel = syscall.MustLoadDLL("kernel32.dll") + CreateToolhelp32Snapshot = kernel.MustFindProc("CreateToolhelp32Snapshot") + Process32First = kernel.MustFindProc("Process32FirstW") + Process32Next = kernel.MustFindProc("Process32NextW") +) + +// ProcessEntry32 structure defined by the Win32 API +type processEntry32 struct { + dwSize uint32 + cntUsage uint32 + th32ProcessID uint32 + th32DefaultHeapID int + th32ModuleID uint32 + cntThreads uint32 + th32ParentProcessID uint32 + pcPriClassBase int32 + dwFlags uint32 + szExeFile [syscall.MAX_PATH]uint16 +} + +func getProcessEntry(pid int) (pe *processEntry32, err error) { + snapshot, _, e1 := CreateToolhelp32Snapshot.Call(th32cs_snapprocess, uintptr(0)) + if snapshot == uintptr(syscall.InvalidHandle) { + err = fmt.Errorf("CreateToolhelp32Snapshot: %v", e1) + return + } + defer syscall.CloseHandle(syscall.Handle(snapshot)) + + var processEntry processEntry32 + processEntry.dwSize = uint32(unsafe.Sizeof(processEntry)) + ok, _, e1 := Process32First.Call(snapshot, uintptr(unsafe.Pointer(&processEntry))) + if ok == 0 { + err = fmt.Errorf("Process32First: %v", e1) + return + } + + for { + if processEntry.th32ProcessID == uint32(pid) { + pe = &processEntry + return + } + + ok, _, e1 = Process32Next.Call(snapshot, uintptr(unsafe.Pointer(&processEntry))) + if ok == 0 { + err = fmt.Errorf("Process32Next: %v", e1) + return + } + } +} + +func getppid() (pid int, err error) { + pe, err := getProcessEntry(os.Getpid()) + if err != nil { + return + } + + pid = int(pe.th32ParentProcessID) + return +} + +// StartedByExplorer returns true if the program was invoked by the user double-clicking +// on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +func StartedByExplorer() bool { + ppid, err := getppid() + if err != nil { + return false + } + + pe, err := getProcessEntry(ppid) + if err != nil { + return false + } + + name := syscall.UTF16ToString(pe.szExeFile[:]) + return name == "explorer.exe" +} diff --git a/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go new file mode 100644 index 0000000..9a28e57 --- /dev/null +++ b/cmd/bleve/vendor/github.com/inconshreveable/mousetrap/trap_windows_1.4.go @@ -0,0 +1,46 @@ +// +build windows +// +build go1.4 + +package mousetrap + +import ( + "os" + "syscall" + "unsafe" +) + +func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) { + snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) + if err != nil { + return nil, err + } + defer syscall.CloseHandle(snapshot) + var procEntry syscall.ProcessEntry32 + procEntry.Size = uint32(unsafe.Sizeof(procEntry)) + if err = syscall.Process32First(snapshot, &procEntry); err != nil { + return nil, err + } + for { + if procEntry.ProcessID == uint32(pid) { + return &procEntry, nil + } + err = syscall.Process32Next(snapshot, &procEntry) + if err != nil { + return nil, err + } + } +} + +// StartedByExplorer returns true if the program was invoked by the user double-clicking +// on the executable from explorer.exe +// +// It is conservative and returns false if any of the internal calls fail. +// It does not guarantee that the program was run from a terminal. It only can tell you +// whether it was launched from explorer.exe +func StartedByExplorer() bool { + pe, err := getProcessEntry(os.Getppid()) + if err != nil { + return false + } + return "explorer.exe" == syscall.UTF16ToString(pe.ExeFile[:]) +} diff --git a/cmd/bleve/vendor/github.com/spf13/cobra/LICENSE.txt b/cmd/bleve/vendor/github.com/spf13/cobra/LICENSE.txt new file mode 100644 index 0000000..298f0e2 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/cobra/LICENSE.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/cmd/bleve/vendor/github.com/spf13/cobra/bash_completions.go b/cmd/bleve/vendor/github.com/spf13/cobra/bash_completions.go new file mode 100644 index 0000000..8820ba8 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/cobra/bash_completions.go @@ -0,0 +1,645 @@ +package cobra + +import ( + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/spf13/pflag" +) + +// Annotations for Bash completion. +const ( + BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions" + BashCompCustom = "cobra_annotation_bash_completion_custom" + BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag" + BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir" +) + +func preamble(out io.Writer, name string) error { + _, err := fmt.Fprintf(out, "# bash completion for %-36s -*- shell-script -*-\n", name) + if err != nil { + return err + } + preamStr := ` +__debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Homebrew on Macs have version 1.3 of bash-completion which doesn't include +# _init_completion. This is a very minimal version of that function. +__my_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +__index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 +} + +__contains_word() +{ + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 +} + +__handle_reply() +{ + __debug "${FUNCNAME[0]}" + case $cur in + -*) + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + local allflags + if [ ${#must_have_one_flag[@]} -ne 0 ]; then + allflags=("${must_have_one_flag[@]}") + else + allflags=("${flags[*]} ${two_word_flags[*]}") + fi + COMPREPLY=( $(compgen -W "${allflags[*]}" -- "$cur") ) + if [[ $(type -t compopt) = "builtin" ]]; then + [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace + fi + + # complete after --flag=abc + if [[ $cur == *=* ]]; then + if [[ $(type -t compopt) = "builtin" ]]; then + compopt +o nospace + fi + + local index flag + flag="${cur%%=*}" + __index_of_word "${flag}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + COMPREPLY=() + PREFIX="" + cur="${cur#*=}" + ${flags_completion[${index}]} + if [ -n "${ZSH_VERSION}" ]; then + # zfs completion needs --flag= prefix + eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" + fi + fi + fi + return 0; + ;; + esac + + # check if we are handling a flag with special work handling + local index + __index_of_word "${prev}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + ${flags_completion[${index}]} + return + fi + + # we are parsing a flag and don't have a special handler, no completion + if [[ ${cur} != "${words[cword]}" ]]; then + return + fi + + local completions + completions=("${commands[@]}") + if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then + completions=("${must_have_one_noun[@]}") + fi + if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then + completions+=("${must_have_one_flag[@]}") + fi + COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") ) + + if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then + COMPREPLY=( $(compgen -W "${noun_aliases[*]}" -- "$cur") ) + fi + + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + declare -F __custom_func >/dev/null && __custom_func + fi + + __ltrim_colon_completions "$cur" +} + +# The arguments should be in the form "ext1|ext2|extn" +__handle_filename_extension_flag() +{ + local ext="$1" + _filedir "@(${ext})" +} + +__handle_subdirs_in_dir_flag() +{ + local dir="$1" + pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 +} + +__handle_flag() +{ + __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + # if a command required a flag, and we found it, unset must_have_one_flag() + local flagname=${words[c]} + local flagvalue + # if the word contained an = + if [[ ${words[c]} == *"="* ]]; then + flagvalue=${flagname#*=} # take in as flagvalue after the = + flagname=${flagname%%=*} # strip everything after the = + flagname="${flagname}=" # but put the = back + fi + __debug "${FUNCNAME[0]}: looking for ${flagname}" + if __contains_word "${flagname}" "${must_have_one_flag[@]}"; then + must_have_one_flag=() + fi + + # if you set a flag which only applies to this command, don't show subcommands + if __contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then + commands=() + fi + + # keep flag value with flagname as flaghash + if [ -n "${flagvalue}" ] ; then + flaghash[${flagname}]=${flagvalue} + elif [ -n "${words[ $((c+1)) ]}" ] ; then + flaghash[${flagname}]=${words[ $((c+1)) ]} + else + flaghash[${flagname}]="true" # pad "true" for bool flag + fi + + # skip the argument to a two word flag + if __contains_word "${words[c]}" "${two_word_flags[@]}"; then + c=$((c+1)) + # if we are looking for a flags value, don't show commands + if [[ $c -eq $cword ]]; then + commands=() + fi + fi + + c=$((c+1)) + +} + +__handle_noun() +{ + __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + if __contains_word "${words[c]}" "${must_have_one_noun[@]}"; then + must_have_one_noun=() + elif __contains_word "${words[c]}" "${noun_aliases[@]}"; then + must_have_one_noun=() + fi + + nouns+=("${words[c]}") + c=$((c+1)) +} + +__handle_command() +{ + __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_$(basename "${words[c]//:/__}")" + else + next_command="_${words[c]//:/__}" + fi + fi + c=$((c+1)) + __debug "${FUNCNAME[0]}: looking for ${next_command}" + declare -F $next_command >/dev/null && $next_command +} + +__handle_word() +{ + if [[ $c -ge $cword ]]; then + __handle_reply + return + fi + __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + if [[ "${words[c]}" == -* ]]; then + __handle_flag + elif __contains_word "${words[c]}" "${commands[@]}"; then + __handle_command + elif [[ $c -eq 0 ]] && __contains_word "$(basename "${words[c]}")" "${commands[@]}"; then + __handle_command + else + __handle_noun + fi + __handle_word +} + +` + _, err = fmt.Fprint(out, preamStr) + return err +} + +func postscript(w io.Writer, name string) error { + name = strings.Replace(name, ":", "__", -1) + _, err := fmt.Fprintf(w, "__start_%s()\n", name) + if err != nil { + return err + } + _, err = fmt.Fprintf(w, `{ + local cur prev words cword + declare -A flaghash 2>/dev/null || : + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -s || return + else + __my_init_completion -n "=" || return + fi + + local c=0 + local flags=() + local two_word_flags=() + local local_nonpersistent_flags=() + local flags_with_completion=() + local flags_completion=() + local commands=("%s") + local must_have_one_flag=() + local must_have_one_noun=() + local last_command + local nouns=() + + __handle_word +} + +`, name) + if err != nil { + return err + } + _, err = fmt.Fprintf(w, `if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_%s %s +else + complete -o default -o nospace -F __start_%s %s +fi + +`, name, name, name, name) + if err != nil { + return err + } + _, err = fmt.Fprintf(w, "# ex: ts=4 sw=4 et filetype=sh\n") + return err +} + +func writeCommands(cmd *Command, w io.Writer) error { + if _, err := fmt.Fprintf(w, " commands=()\n"); err != nil { + return err + } + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c == cmd.helpCommand { + continue + } + if _, err := fmt.Fprintf(w, " commands+=(%q)\n", c.Name()); err != nil { + return err + } + } + _, err := fmt.Fprintf(w, "\n") + return err +} + +func writeFlagHandler(name string, annotations map[string][]string, w io.Writer) error { + for key, value := range annotations { + switch key { + case BashCompFilenameExt: + _, err := fmt.Fprintf(w, " flags_with_completion+=(%q)\n", name) + if err != nil { + return err + } + + if len(value) > 0 { + ext := "__handle_filename_extension_flag " + strings.Join(value, "|") + _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext) + } else { + ext := "_filedir" + _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext) + } + if err != nil { + return err + } + case BashCompCustom: + _, err := fmt.Fprintf(w, " flags_with_completion+=(%q)\n", name) + if err != nil { + return err + } + if len(value) > 0 { + handlers := strings.Join(value, "; ") + _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", handlers) + } else { + _, err = fmt.Fprintf(w, " flags_completion+=(:)\n") + } + if err != nil { + return err + } + case BashCompSubdirsInDir: + _, err := fmt.Fprintf(w, " flags_with_completion+=(%q)\n", name) + + if len(value) == 1 { + ext := "__handle_subdirs_in_dir_flag " + value[0] + _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext) + } else { + ext := "_filedir -d" + _, err = fmt.Fprintf(w, " flags_completion+=(%q)\n", ext) + } + if err != nil { + return err + } + } + } + return nil +} + +func writeShortFlag(flag *pflag.Flag, w io.Writer) error { + b := (len(flag.NoOptDefVal) > 0) + name := flag.Shorthand + format := " " + if !b { + format += "two_word_" + } + format += "flags+=(\"-%s\")\n" + if _, err := fmt.Fprintf(w, format, name); err != nil { + return err + } + return writeFlagHandler("-"+name, flag.Annotations, w) +} + +func writeFlag(flag *pflag.Flag, w io.Writer) error { + b := (len(flag.NoOptDefVal) > 0) + name := flag.Name + format := " flags+=(\"--%s" + if !b { + format += "=" + } + format += "\")\n" + if _, err := fmt.Fprintf(w, format, name); err != nil { + return err + } + return writeFlagHandler("--"+name, flag.Annotations, w) +} + +func writeLocalNonPersistentFlag(flag *pflag.Flag, w io.Writer) error { + b := (len(flag.NoOptDefVal) > 0) + name := flag.Name + format := " local_nonpersistent_flags+=(\"--%s" + if !b { + format += "=" + } + format += "\")\n" + _, err := fmt.Fprintf(w, format, name) + return err +} + +func writeFlags(cmd *Command, w io.Writer) error { + _, err := fmt.Fprintf(w, ` flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + +`) + if err != nil { + return err + } + localNonPersistentFlags := cmd.LocalNonPersistentFlags() + var visitErr error + cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + if err := writeFlag(flag, w); err != nil { + visitErr = err + return + } + if len(flag.Shorthand) > 0 { + if err := writeShortFlag(flag, w); err != nil { + visitErr = err + return + } + } + if localNonPersistentFlags.Lookup(flag.Name) != nil { + if err := writeLocalNonPersistentFlag(flag, w); err != nil { + visitErr = err + return + } + } + }) + if visitErr != nil { + return visitErr + } + cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + if err := writeFlag(flag, w); err != nil { + visitErr = err + return + } + if len(flag.Shorthand) > 0 { + if err := writeShortFlag(flag, w); err != nil { + visitErr = err + return + } + } + }) + if visitErr != nil { + return visitErr + } + + _, err = fmt.Fprintf(w, "\n") + return err +} + +func writeRequiredFlag(cmd *Command, w io.Writer) error { + if _, err := fmt.Fprintf(w, " must_have_one_flag=()\n"); err != nil { + return err + } + flags := cmd.NonInheritedFlags() + var visitErr error + flags.VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + for key := range flag.Annotations { + switch key { + case BashCompOneRequiredFlag: + format := " must_have_one_flag+=(\"--%s" + b := (flag.Value.Type() == "bool") + if !b { + format += "=" + } + format += "\")\n" + if _, err := fmt.Fprintf(w, format, flag.Name); err != nil { + visitErr = err + return + } + + if len(flag.Shorthand) > 0 { + if _, err := fmt.Fprintf(w, " must_have_one_flag+=(\"-%s\")\n", flag.Shorthand); err != nil { + visitErr = err + return + } + } + } + } + }) + return visitErr +} + +func writeRequiredNouns(cmd *Command, w io.Writer) error { + if _, err := fmt.Fprintf(w, " must_have_one_noun=()\n"); err != nil { + return err + } + sort.Sort(sort.StringSlice(cmd.ValidArgs)) + for _, value := range cmd.ValidArgs { + if _, err := fmt.Fprintf(w, " must_have_one_noun+=(%q)\n", value); err != nil { + return err + } + } + return nil +} + +func writeArgAliases(cmd *Command, w io.Writer) error { + if _, err := fmt.Fprintf(w, " noun_aliases=()\n"); err != nil { + return err + } + sort.Sort(sort.StringSlice(cmd.ArgAliases)) + for _, value := range cmd.ArgAliases { + if _, err := fmt.Fprintf(w, " noun_aliases+=(%q)\n", value); err != nil { + return err + } + } + return nil +} + +func gen(cmd *Command, w io.Writer) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c == cmd.helpCommand { + continue + } + if err := gen(c, w); err != nil { + return err + } + } + commandName := cmd.CommandPath() + commandName = strings.Replace(commandName, " ", "_", -1) + commandName = strings.Replace(commandName, ":", "__", -1) + if _, err := fmt.Fprintf(w, "_%s()\n{\n", commandName); err != nil { + return err + } + if _, err := fmt.Fprintf(w, " last_command=%q\n", commandName); err != nil { + return err + } + if err := writeCommands(cmd, w); err != nil { + return err + } + if err := writeFlags(cmd, w); err != nil { + return err + } + if err := writeRequiredFlag(cmd, w); err != nil { + return err + } + if err := writeRequiredNouns(cmd, w); err != nil { + return err + } + if err := writeArgAliases(cmd, w); err != nil { + return err + } + if _, err := fmt.Fprintf(w, "}\n\n"); err != nil { + return err + } + return nil +} + +// GenBashCompletion generates bash completion file and writes to the passed writer. +func (cmd *Command) GenBashCompletion(w io.Writer) error { + if err := preamble(w, cmd.Name()); err != nil { + return err + } + if len(cmd.BashCompletionFunction) > 0 { + if _, err := fmt.Fprintf(w, "%s\n", cmd.BashCompletionFunction); err != nil { + return err + } + } + if err := gen(cmd, w); err != nil { + return err + } + return postscript(w, cmd.Name()) +} + +func nonCompletableFlag(flag *pflag.Flag) bool { + return flag.Hidden || len(flag.Deprecated) > 0 +} + +// GenBashCompletionFile generates bash completion file. +func (cmd *Command) GenBashCompletionFile(filename string) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return cmd.GenBashCompletion(outFile) +} + +// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag, if it exists. +func (cmd *Command) MarkFlagRequired(name string) error { + return MarkFlagRequired(cmd.Flags(), name) +} + +// MarkPersistentFlagRequired adds the BashCompOneRequiredFlag annotation to the named persistent flag, if it exists. +func (cmd *Command) MarkPersistentFlagRequired(name string) error { + return MarkFlagRequired(cmd.PersistentFlags(), name) +} + +// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag in the flag set, if it exists. +func MarkFlagRequired(flags *pflag.FlagSet, name string) error { + return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"}) +} + +// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists. +// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. +func (cmd *Command) MarkFlagFilename(name string, extensions ...string) error { + return MarkFlagFilename(cmd.Flags(), name, extensions...) +} + +// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists. +// Generated bash autocompletion will call the bash function f for the flag. +func (cmd *Command) MarkFlagCustom(name string, f string) error { + return MarkFlagCustom(cmd.Flags(), name, f) +} + +// MarkPersistentFlagFilename adds the BashCompFilenameExt annotation to the named persistent flag, if it exists. +// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. +func (cmd *Command) MarkPersistentFlagFilename(name string, extensions ...string) error { + return MarkFlagFilename(cmd.PersistentFlags(), name, extensions...) +} + +// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag in the flag set, if it exists. +// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided. +func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error { + return flags.SetAnnotation(name, BashCompFilenameExt, extensions) +} + +// MarkFlagCustom adds the BashCompCustom annotation to the named flag in the flag set, if it exists. +// Generated bash autocompletion will call the bash function f for the flag. +func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error { + return flags.SetAnnotation(name, BashCompCustom, []string{f}) +} diff --git a/cmd/bleve/vendor/github.com/spf13/cobra/cobra.go b/cmd/bleve/vendor/github.com/spf13/cobra/cobra.go new file mode 100644 index 0000000..9605b98 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/cobra/cobra.go @@ -0,0 +1,174 @@ +// Copyright © 2013 Steve Francia . +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Commands similar to git, go tools and other modern CLI tools +// inspired by go, go-Commander, gh and subcommand + +package cobra + +import ( + "fmt" + "io" + "reflect" + "strconv" + "strings" + "text/template" + "unicode" +) + +var templateFuncs = template.FuncMap{ + "trim": strings.TrimSpace, + "trimRightSpace": trimRightSpace, + "appendIfNotPresent": appendIfNotPresent, + "rpad": rpad, + "gt": Gt, + "eq": Eq, +} + +var initializers []func() + +// EnablePrefixMatching allows to set automatic prefix matching. Automatic prefix matching can be a dangerous thing +// to automatically enable in CLI tools. +// Set this to true to enable it. +var EnablePrefixMatching = false + +// EnableCommandSorting controls sorting of the slice of commands, which is turned on by default. +// To disable sorting, set it to false. +var EnableCommandSorting = true + +// AddTemplateFunc adds a template function that's available to Usage and Help +// template generation. +func AddTemplateFunc(name string, tmplFunc interface{}) { + templateFuncs[name] = tmplFunc +} + +// AddTemplateFuncs adds multiple template functions availalble to Usage and +// Help template generation. +func AddTemplateFuncs(tmplFuncs template.FuncMap) { + for k, v := range tmplFuncs { + templateFuncs[k] = v + } +} + +// OnInitialize takes a series of func() arguments and appends them to a slice of func(). +func OnInitialize(y ...func()) { + initializers = append(initializers, y...) +} + +// Gt takes two types and checks whether the first type is greater than the second. In case of types Arrays, Chans, +// Maps and Slices, Gt will compare their lengths. Ints are compared directly while strings are first parsed as +// ints and then compared. +func Gt(a interface{}, b interface{}) bool { + var left, right int64 + av := reflect.ValueOf(a) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + left = int64(av.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + left = av.Int() + case reflect.String: + left, _ = strconv.ParseInt(av.String(), 10, 64) + } + + bv := reflect.ValueOf(b) + + switch bv.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + right = int64(bv.Len()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + right = bv.Int() + case reflect.String: + right, _ = strconv.ParseInt(bv.String(), 10, 64) + } + + return left > right +} + +// Eq takes two types and checks whether they are equal. Supported types are int and string. Unsupported types will panic. +func Eq(a interface{}, b interface{}) bool { + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + + switch av.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + panic("Eq called on unsupported type") + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return av.Int() == bv.Int() + case reflect.String: + return av.String() == bv.String() + } + return false +} + +func trimRightSpace(s string) string { + return strings.TrimRightFunc(s, unicode.IsSpace) +} + +// appendIfNotPresent will append stringToAppend to the end of s, but only if it's not yet present in s. +func appendIfNotPresent(s, stringToAppend string) string { + if strings.Contains(s, stringToAppend) { + return s + } + return s + " " + stringToAppend +} + +// rpad adds padding to the right of a string. +func rpad(s string, padding int) string { + template := fmt.Sprintf("%%-%ds", padding) + return fmt.Sprintf(template, s) +} + +// tmpl executes the given template text on data, writing the result to w. +func tmpl(w io.Writer, text string, data interface{}) error { + t := template.New("top") + t.Funcs(templateFuncs) + template.Must(t.Parse(text)) + return t.Execute(w, data) +} + +// ld compares two strings and returns the levenshtein distance between them. +func ld(s, t string, ignoreCase bool) int { + if ignoreCase { + s = strings.ToLower(s) + t = strings.ToLower(t) + } + d := make([][]int, len(s)+1) + for i := range d { + d[i] = make([]int, len(t)+1) + } + for i := range d { + d[i][0] = i + } + for j := range d[0] { + d[0][j] = j + } + for j := 1; j <= len(t); j++ { + for i := 1; i <= len(s); i++ { + if s[i-1] == t[j-1] { + d[i][j] = d[i-1][j-1] + } else { + min := d[i-1][j] + if d[i][j-1] < min { + min = d[i][j-1] + } + if d[i-1][j-1] < min { + min = d[i-1][j-1] + } + d[i][j] = min + 1 + } + } + + } + return d[len(s)][len(t)] +} diff --git a/cmd/bleve/vendor/github.com/spf13/cobra/command.go b/cmd/bleve/vendor/github.com/spf13/cobra/command.go new file mode 100644 index 0000000..5c1c3a0 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/cobra/command.go @@ -0,0 +1,1309 @@ +// Copyright © 2013 Steve Francia . +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//Package cobra is a commander providing a simple interface to create powerful modern CLI interfaces. +//In addition to providing an interface, Cobra simultaneously provides a controller to organize your application code. +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + flag "github.com/spf13/pflag" +) + +// Command is just that, a command for your application. +// eg. 'go run' ... 'run' is the command. Cobra requires +// you to define the usage and description as part of your command +// definition to ensure usability. +type Command struct { + // Name is the command name, usually the executable's name. + name string + // The one-line usage message. + Use string + // An array of aliases that can be used instead of the first word in Use. + Aliases []string + // An array of command names for which this command will be suggested - similar to aliases but only suggests. + SuggestFor []string + // The short description shown in the 'help' output. + Short string + // The long message shown in the 'help ' output. + Long string + // Examples of how to use the command + Example string + // List of all valid non-flag arguments that are accepted in bash completions + ValidArgs []string + // List of aliases for ValidArgs. These are not suggested to the user in the bash + // completion, but accepted if entered manually. + ArgAliases []string + // Custom functions used by the bash autocompletion generator + BashCompletionFunction string + // Is this command deprecated and should print this string when used? + Deprecated string + // Is this command hidden and should NOT show up in the list of available commands? + Hidden bool + // Annotations are key/value pairs that can be used by applications to identify or + // group commands + Annotations map[string]string + // Full set of flags + flags *flag.FlagSet + // Set of flags childrens of this command will inherit + pflags *flag.FlagSet + // Flags that are declared specifically by this command (not inherited). + lflags *flag.FlagSet + // SilenceErrors is an option to quiet errors down stream + SilenceErrors bool + // Silence Usage is an option to silence usage when an error occurs. + SilenceUsage bool + // The *Run functions are executed in the following order: + // * PersistentPreRun() + // * PreRun() + // * Run() + // * PostRun() + // * PersistentPostRun() + // All functions get the same args, the arguments after the command name + // PersistentPreRun: children of this command will inherit and execute + PersistentPreRun func(cmd *Command, args []string) + // PersistentPreRunE: PersistentPreRun but returns an error + PersistentPreRunE func(cmd *Command, args []string) error + // PreRun: children of this command will not inherit. + PreRun func(cmd *Command, args []string) + // PreRunE: PreRun but returns an error + PreRunE func(cmd *Command, args []string) error + // Run: Typically the actual work function. Most commands will only implement this + Run func(cmd *Command, args []string) + // RunE: Run but returns an error + RunE func(cmd *Command, args []string) error + // PostRun: run after the Run command. + PostRun func(cmd *Command, args []string) + // PostRunE: PostRun but returns an error + PostRunE func(cmd *Command, args []string) error + // PersistentPostRun: children of this command will inherit and execute after PostRun + PersistentPostRun func(cmd *Command, args []string) + // PersistentPostRunE: PersistentPostRun but returns an error + PersistentPostRunE func(cmd *Command, args []string) error + // DisableAutoGenTag remove + DisableAutoGenTag bool + // Commands is the list of commands supported by this program. + commands []*Command + // Parent Command for this command + parent *Command + // max lengths of commands' string lengths for use in padding + commandsMaxUseLen int + commandsMaxCommandPathLen int + commandsMaxNameLen int + // is commands slice are sorted or not + commandsAreSorted bool + + flagErrorBuf *bytes.Buffer + + args []string // actual args parsed from flags + output *io.Writer // out writer if set in SetOutput(w) + usageFunc func(*Command) error // Usage can be defined by application + usageTemplate string // Can be defined by Application + flagErrorFunc func(*Command, error) error + helpTemplate string // Can be defined by Application + helpFunc func(*Command, []string) // Help can be defined by application + helpCommand *Command // The help command + // The global normalization function that we can use on every pFlag set and children commands + globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName + + // Disable the suggestions based on Levenshtein distance that go along with 'unknown command' messages + DisableSuggestions bool + // If displaying suggestions, allows to set the minimum levenshtein distance to display, must be > 0 + SuggestionsMinimumDistance int + + // Disable the flag parsing. If this is true all flags will be passed to the command as arguments. + DisableFlagParsing bool +} + +// SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden +// particularly useful when testing. +func (c *Command) SetArgs(a []string) { + c.args = a +} + +// SetOutput sets the destination for usage and error messages. +// If output is nil, os.Stderr is used. +func (c *Command) SetOutput(output io.Writer) { + c.output = &output +} + +// SetUsageFunc sets usage function. Usage can be defined by application. +func (c *Command) SetUsageFunc(f func(*Command) error) { + c.usageFunc = f +} + +// SetUsageTemplate sets usage template. Can be defined by Application. +func (c *Command) SetUsageTemplate(s string) { + c.usageTemplate = s +} + +// SetFlagErrorFunc sets a function to generate an error when flag parsing +// fails +func (c *Command) SetFlagErrorFunc(f func(*Command, error) error) { + c.flagErrorFunc = f +} + +// SetHelpFunc sets help function. Can be defined by Application +func (c *Command) SetHelpFunc(f func(*Command, []string)) { + c.helpFunc = f +} + +// SetHelpCommand sets help command. +func (c *Command) SetHelpCommand(cmd *Command) { + c.helpCommand = cmd +} + +// SetHelpTemplate sets help template to be used. Application can use it to set custom template. +func (c *Command) SetHelpTemplate(s string) { + c.helpTemplate = s +} + +// SetGlobalNormalizationFunc sets a normalization function to all flag sets and also to child commands. +// The user should not have a cyclic dependency on commands. +func (c *Command) SetGlobalNormalizationFunc(n func(f *flag.FlagSet, name string) flag.NormalizedName) { + c.Flags().SetNormalizeFunc(n) + c.PersistentFlags().SetNormalizeFunc(n) + c.globNormFunc = n + + for _, command := range c.commands { + command.SetGlobalNormalizationFunc(n) + } +} + +// OutOrStdout returns output to stdout +func (c *Command) OutOrStdout() io.Writer { + return c.getOut(os.Stdout) +} + +// OutOrStderr returns output to stderr +func (c *Command) OutOrStderr() io.Writer { + return c.getOut(os.Stderr) +} + +func (c *Command) getOut(def io.Writer) io.Writer { + if c.output != nil { + return *c.output + } + if c.HasParent() { + return c.parent.getOut(def) + } + return def +} + +// UsageFunc returns either the function set by SetUsageFunc for this command +// or a parent, or it returns a default usage function. +func (c *Command) UsageFunc() (f func(*Command) error) { + if c.usageFunc != nil { + return c.usageFunc + } + + if c.HasParent() { + return c.parent.UsageFunc() + } + return func(c *Command) error { + c.mergePersistentFlags() + err := tmpl(c.OutOrStderr(), c.UsageTemplate(), c) + if err != nil { + c.Println(err) + } + return err + } +} + +// Usage puts out the usage for the command. +// Used when a user provides invalid input. +// Can be defined by user by overriding UsageFunc. +func (c *Command) Usage() error { + return c.UsageFunc()(c) +} + +// HelpFunc returns either the function set by SetHelpFunc for this command +// or a parent, or it returns a function with default help behavior. +func (c *Command) HelpFunc() func(*Command, []string) { + if helpFunc := c.checkHelpFunc(); helpFunc != nil { + return helpFunc + } + return func(*Command, []string) { + c.mergePersistentFlags() + err := tmpl(c.OutOrStdout(), c.HelpTemplate(), c) + if err != nil { + c.Println(err) + } + } +} + +// checkHelpFunc checks if there is helpFunc in ancestors of c. +func (c *Command) checkHelpFunc() func(*Command, []string) { + if c == nil { + return nil + } + if c.helpFunc != nil { + return c.helpFunc + } + if c.HasParent() { + return c.parent.checkHelpFunc() + } + return nil +} + +// Help puts out the help for the command. +// Used when a user calls help [command]. +// Can be defined by user by overriding HelpFunc. +func (c *Command) Help() error { + c.HelpFunc()(c, []string{}) + return nil +} + +// UsageString return usage string. +func (c *Command) UsageString() string { + tmpOutput := c.output + bb := new(bytes.Buffer) + c.SetOutput(bb) + c.Usage() + c.output = tmpOutput + return bb.String() +} + +// FlagErrorFunc returns either the function set by SetFlagErrorFunc for this +// command or a parent, or it returns a function which returns the original +// error. +func (c *Command) FlagErrorFunc() (f func(*Command, error) error) { + if c.flagErrorFunc != nil { + return c.flagErrorFunc + } + + if c.HasParent() { + return c.parent.FlagErrorFunc() + } + return func(c *Command, err error) error { + return err + } +} + +var minUsagePadding = 25 + +// UsagePadding return padding for the usage. +func (c *Command) UsagePadding() int { + if c.parent == nil || minUsagePadding > c.parent.commandsMaxUseLen { + return minUsagePadding + } + return c.parent.commandsMaxUseLen +} + +var minCommandPathPadding = 11 + +// CommandPathPadding return padding for the command path. +func (c *Command) CommandPathPadding() int { + if c.parent == nil || minCommandPathPadding > c.parent.commandsMaxCommandPathLen { + return minCommandPathPadding + } + return c.parent.commandsMaxCommandPathLen +} + +var minNamePadding = 11 + +// NamePadding returns padding for the name. +func (c *Command) NamePadding() int { + if c.parent == nil || minNamePadding > c.parent.commandsMaxNameLen { + return minNamePadding + } + return c.parent.commandsMaxNameLen +} + +// UsageTemplate returns usage template for the command. +func (c *Command) UsageTemplate() string { + if c.usageTemplate != "" { + return c.usageTemplate + } + + if c.HasParent() { + return c.parent.UsageTemplate() + } + return `Usage:{{if .Runnable}} + {{if .HasAvailableFlags}}{{appendIfNotPresent .UseLine "[flags]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + {{ .CommandPath}} [command]{{end}}{{if gt .Aliases 0}} + +Aliases: + {{.NameAndAliases}} +{{end}}{{if .HasExample}} + +Examples: +{{ .Example }}{{end}}{{ if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if .IsAvailableCommand}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimRightSpace}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsHelpCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasAvailableSubCommands }} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` +} + +// HelpTemplate return help template for the command. +func (c *Command) HelpTemplate() string { + if c.helpTemplate != "" { + return c.helpTemplate + } + + if c.HasParent() { + return c.parent.HelpTemplate() + } + return `{{with or .Long .Short }}{{. | trim}} + +{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` +} + +// Really only used when casting a command to a commander. +func (c *Command) resetChildrensParents() { + for _, x := range c.commands { + x.parent = c + } +} + +// Test if the named flag is a boolean flag. +func isBooleanFlag(name string, f *flag.FlagSet) bool { + flag := f.Lookup(name) + if flag == nil { + return false + } + return flag.Value.Type() == "bool" +} + +// Test if the named flag is a boolean flag. +func isBooleanShortFlag(name string, f *flag.FlagSet) bool { + result := false + f.VisitAll(func(f *flag.Flag) { + if f.Shorthand == name && f.Value.Type() == "bool" { + result = true + } + }) + return result +} + +func stripFlags(args []string, c *Command) []string { + if len(args) < 1 { + return args + } + c.mergePersistentFlags() + + commands := []string{} + + inQuote := false + inFlag := false + for _, y := range args { + if !inQuote { + switch { + case strings.HasPrefix(y, "\""): + inQuote = true + case strings.Contains(y, "=\""): + inQuote = true + case strings.HasPrefix(y, "--") && !strings.Contains(y, "="): + // TODO: this isn't quite right, we should really check ahead for 'true' or 'false' + inFlag = !isBooleanFlag(y[2:], c.Flags()) + case strings.HasPrefix(y, "-") && !strings.Contains(y, "=") && len(y) == 2 && !isBooleanShortFlag(y[1:], c.Flags()): + inFlag = true + case inFlag: + inFlag = false + case y == "": + // strip empty commands, as the go tests expect this to be ok.... + case !strings.HasPrefix(y, "-"): + commands = append(commands, y) + inFlag = false + } + } + + if strings.HasSuffix(y, "\"") && !strings.HasSuffix(y, "\\\"") { + inQuote = false + } + } + + return commands +} + +// argsMinusFirstX removes only the first x from args. Otherwise, commands that look like +// openshift admin policy add-role-to-user admin my-user, lose the admin argument (arg[4]). +func argsMinusFirstX(args []string, x string) []string { + for i, y := range args { + if x == y { + ret := []string{} + ret = append(ret, args[:i]...) + ret = append(ret, args[i+1:]...) + return ret + } + } + return args +} + +// Find the target command given the args and command tree +// Meant to be run on the highest node. Only searches down. +func (c *Command) Find(args []string) (*Command, []string, error) { + if c == nil { + return nil, nil, fmt.Errorf("Called find() on a nil Command") + } + + var innerfind func(*Command, []string) (*Command, []string) + + innerfind = func(c *Command, innerArgs []string) (*Command, []string) { + argsWOflags := stripFlags(innerArgs, c) + if len(argsWOflags) == 0 { + return c, innerArgs + } + nextSubCmd := argsWOflags[0] + matches := make([]*Command, 0) + for _, cmd := range c.commands { + if cmd.Name() == nextSubCmd || cmd.HasAlias(nextSubCmd) { // exact name or alias match + return innerfind(cmd, argsMinusFirstX(innerArgs, nextSubCmd)) + } + if EnablePrefixMatching { + if strings.HasPrefix(cmd.Name(), nextSubCmd) { // prefix match + matches = append(matches, cmd) + } + for _, x := range cmd.Aliases { + if strings.HasPrefix(x, nextSubCmd) { + matches = append(matches, cmd) + } + } + } + } + + // only accept a single prefix match - multiple matches would be ambiguous + if len(matches) == 1 { + return innerfind(matches[0], argsMinusFirstX(innerArgs, argsWOflags[0])) + } + + return c, innerArgs + } + + commandFound, a := innerfind(c, args) + argsWOflags := stripFlags(a, commandFound) + + // no subcommand, always take args + if !commandFound.HasSubCommands() { + return commandFound, a, nil + } + + // root command with subcommands, do subcommand checking + if commandFound == c && len(argsWOflags) > 0 { + suggestionsString := "" + if !c.DisableSuggestions { + if c.SuggestionsMinimumDistance <= 0 { + c.SuggestionsMinimumDistance = 2 + } + if suggestions := c.SuggestionsFor(argsWOflags[0]); len(suggestions) > 0 { + suggestionsString += "\n\nDid you mean this?\n" + for _, s := range suggestions { + suggestionsString += fmt.Sprintf("\t%v\n", s) + } + } + } + return commandFound, a, fmt.Errorf("unknown command %q for %q%s", argsWOflags[0], commandFound.CommandPath(), suggestionsString) + } + + return commandFound, a, nil +} + +// SuggestionsFor provides suggestions for the typedName. +func (c *Command) SuggestionsFor(typedName string) []string { + suggestions := []string{} + for _, cmd := range c.commands { + if cmd.IsAvailableCommand() { + levenshteinDistance := ld(typedName, cmd.Name(), true) + suggestByLevenshtein := levenshteinDistance <= c.SuggestionsMinimumDistance + suggestByPrefix := strings.HasPrefix(strings.ToLower(cmd.Name()), strings.ToLower(typedName)) + if suggestByLevenshtein || suggestByPrefix { + suggestions = append(suggestions, cmd.Name()) + } + for _, explicitSuggestion := range cmd.SuggestFor { + if strings.EqualFold(typedName, explicitSuggestion) { + suggestions = append(suggestions, cmd.Name()) + } + } + } + } + return suggestions +} + +// VisitParents visits all parents of the command and invokes fn on each parent. +func (c *Command) VisitParents(fn func(*Command)) { + var traverse func(*Command) *Command + + traverse = func(x *Command) *Command { + if x != c { + fn(x) + } + if x.HasParent() { + return traverse(x.parent) + } + return x + } + traverse(c) +} + +// Root finds root command. +func (c *Command) Root() *Command { + var findRoot func(*Command) *Command + + findRoot = func(x *Command) *Command { + if x.HasParent() { + return findRoot(x.parent) + } + return x + } + + return findRoot(c) +} + +// ArgsLenAtDash will return the length of f.Args at the moment when a -- was +// found during arg parsing. This allows your program to know which args were +// before the -- and which came after. (Description from +// https://godoc.org/github.com/spf13/pflag#FlagSet.ArgsLenAtDash). +func (c *Command) ArgsLenAtDash() int { + return c.Flags().ArgsLenAtDash() +} + +func (c *Command) execute(a []string) (err error) { + if c == nil { + return fmt.Errorf("Called Execute() on a nil Command") + } + + if len(c.Deprecated) > 0 { + c.Printf("Command %q is deprecated, %s\n", c.Name(), c.Deprecated) + } + + // initialize help flag as the last point possible to allow for user + // overriding + c.initHelpFlag() + + err = c.ParseFlags(a) + if err != nil { + return c.FlagErrorFunc()(c, err) + } + // If help is called, regardless of other flags, return we want help + // Also say we need help if the command isn't runnable. + helpVal, err := c.Flags().GetBool("help") + if err != nil { + // should be impossible to get here as we always declare a help + // flag in initHelpFlag() + c.Println("\"help\" flag declared as non-bool. Please correct your code") + return err + } + + if helpVal || !c.Runnable() { + return flag.ErrHelp + } + + c.preRun() + + argWoFlags := c.Flags().Args() + if c.DisableFlagParsing { + argWoFlags = a + } + + for p := c; p != nil; p = p.Parent() { + if p.PersistentPreRunE != nil { + if err := p.PersistentPreRunE(c, argWoFlags); err != nil { + return err + } + break + } else if p.PersistentPreRun != nil { + p.PersistentPreRun(c, argWoFlags) + break + } + } + if c.PreRunE != nil { + if err := c.PreRunE(c, argWoFlags); err != nil { + return err + } + } else if c.PreRun != nil { + c.PreRun(c, argWoFlags) + } + + if c.RunE != nil { + if err := c.RunE(c, argWoFlags); err != nil { + return err + } + } else { + c.Run(c, argWoFlags) + } + if c.PostRunE != nil { + if err := c.PostRunE(c, argWoFlags); err != nil { + return err + } + } else if c.PostRun != nil { + c.PostRun(c, argWoFlags) + } + for p := c; p != nil; p = p.Parent() { + if p.PersistentPostRunE != nil { + if err := p.PersistentPostRunE(c, argWoFlags); err != nil { + return err + } + break + } else if p.PersistentPostRun != nil { + p.PersistentPostRun(c, argWoFlags) + break + } + } + + return nil +} + +func (c *Command) preRun() { + for _, x := range initializers { + x() + } +} + +func (c *Command) errorMsgFromParse() string { + s := c.flagErrorBuf.String() + + x := strings.Split(s, "\n") + + if len(x) > 0 { + return x[0] + } + return "" +} + +// Execute Call execute to use the args (os.Args[1:] by default) +// and run through the command tree finding appropriate matches +// for commands and then corresponding flags. +func (c *Command) Execute() error { + _, err := c.ExecuteC() + return err +} + +// ExecuteC executes the command. +func (c *Command) ExecuteC() (cmd *Command, err error) { + + // Regardless of what command execute is called on, run on Root only + if c.HasParent() { + return c.Root().ExecuteC() + } + + // windows hook + if preExecHookFn != nil { + preExecHookFn(c) + } + + // initialize help as the last point possible to allow for user + // overriding + c.initHelpCmd() + + var args []string + + // Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155 + if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" { + args = os.Args[1:] + } else { + args = c.args + } + + cmd, flags, err := c.Find(args) + if err != nil { + // If found parse to a subcommand and then failed, talk about the subcommand + if cmd != nil { + c = cmd + } + if !c.SilenceErrors { + c.Println("Error:", err.Error()) + c.Printf("Run '%v --help' for usage.\n", c.CommandPath()) + } + return c, err + } + err = cmd.execute(flags) + if err != nil { + // Always show help if requested, even if SilenceErrors is in + // effect + if err == flag.ErrHelp { + cmd.HelpFunc()(cmd, args) + return cmd, nil + } + + // If root command has SilentErrors flagged, + // all subcommands should respect it + if !cmd.SilenceErrors && !c.SilenceErrors { + c.Println("Error:", err.Error()) + } + + // If root command has SilentUsage flagged, + // all subcommands should respect it + if !cmd.SilenceUsage && !c.SilenceUsage { + c.Println(cmd.UsageString()) + } + return cmd, err + } + return cmd, nil +} + +func (c *Command) initHelpFlag() { + c.mergePersistentFlags() + if c.Flags().Lookup("help") == nil { + c.Flags().BoolP("help", "h", false, "help for "+c.Name()) + } +} + +func (c *Command) initHelpCmd() { + if c.helpCommand == nil { + if !c.HasSubCommands() { + return + } + + c.helpCommand = &Command{ + Use: "help [command]", + Short: "Help about any command", + Long: `Help provides help for any command in the application. + Simply type ` + c.Name() + ` help [path to command] for full details.`, + PersistentPreRun: func(cmd *Command, args []string) {}, + PersistentPostRun: func(cmd *Command, args []string) {}, + + Run: func(c *Command, args []string) { + cmd, _, e := c.Root().Find(args) + if cmd == nil || e != nil { + c.Printf("Unknown help topic %#q.", args) + c.Root().Usage() + } else { + cmd.Help() + } + }, + } + } + c.AddCommand(c.helpCommand) +} + +// ResetCommands used for testing. +func (c *Command) ResetCommands() { + c.commands = nil + c.helpCommand = nil +} + +// Sorts commands by their names. +type commandSorterByName []*Command + +func (c commandSorterByName) Len() int { return len(c) } +func (c commandSorterByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c commandSorterByName) Less(i, j int) bool { return c[i].Name() < c[j].Name() } + +// Commands returns a sorted slice of child commands. +func (c *Command) Commands() []*Command { + // do not sort commands if it already sorted or sorting was disabled + if EnableCommandSorting && !c.commandsAreSorted { + sort.Sort(commandSorterByName(c.commands)) + c.commandsAreSorted = true + } + return c.commands +} + +// AddCommand adds one or more commands to this parent command. +func (c *Command) AddCommand(cmds ...*Command) { + for i, x := range cmds { + if cmds[i] == c { + panic("Command can't be a child of itself") + } + cmds[i].parent = c + // update max lengths + usageLen := len(x.Use) + if usageLen > c.commandsMaxUseLen { + c.commandsMaxUseLen = usageLen + } + commandPathLen := len(x.CommandPath()) + if commandPathLen > c.commandsMaxCommandPathLen { + c.commandsMaxCommandPathLen = commandPathLen + } + nameLen := len(x.Name()) + if nameLen > c.commandsMaxNameLen { + c.commandsMaxNameLen = nameLen + } + // If global normalization function exists, update all children + if c.globNormFunc != nil { + x.SetGlobalNormalizationFunc(c.globNormFunc) + } + c.commands = append(c.commands, x) + c.commandsAreSorted = false + } +} + +// RemoveCommand removes one or more commands from a parent command. +func (c *Command) RemoveCommand(cmds ...*Command) { + commands := []*Command{} +main: + for _, command := range c.commands { + for _, cmd := range cmds { + if command == cmd { + command.parent = nil + continue main + } + } + commands = append(commands, command) + } + c.commands = commands + // recompute all lengths + c.commandsMaxUseLen = 0 + c.commandsMaxCommandPathLen = 0 + c.commandsMaxNameLen = 0 + for _, command := range c.commands { + usageLen := len(command.Use) + if usageLen > c.commandsMaxUseLen { + c.commandsMaxUseLen = usageLen + } + commandPathLen := len(command.CommandPath()) + if commandPathLen > c.commandsMaxCommandPathLen { + c.commandsMaxCommandPathLen = commandPathLen + } + nameLen := len(command.Name()) + if nameLen > c.commandsMaxNameLen { + c.commandsMaxNameLen = nameLen + } + } +} + +// Print is a convenience method to Print to the defined output, fallback to Stderr if not set. +func (c *Command) Print(i ...interface{}) { + fmt.Fprint(c.OutOrStderr(), i...) +} + +// Println is a convenience method to Println to the defined output, fallback to Stderr if not set. +func (c *Command) Println(i ...interface{}) { + str := fmt.Sprintln(i...) + c.Print(str) +} + +// Printf is a convenience method to Printf to the defined output, fallback to Stderr if not set. +func (c *Command) Printf(format string, i ...interface{}) { + str := fmt.Sprintf(format, i...) + c.Print(str) +} + +// CommandPath returns the full path to this command. +func (c *Command) CommandPath() string { + str := c.Name() + x := c + for x.HasParent() { + str = x.parent.Name() + " " + str + x = x.parent + } + return str +} + +// UseLine puts out the full usage for a given command (including parents). +func (c *Command) UseLine() string { + str := "" + if c.HasParent() { + str = c.parent.CommandPath() + " " + } + return str + c.Use +} + +// DebugFlags used to determine which flags have been assigned to which commands +// and which persist. +func (c *Command) DebugFlags() { + c.Println("DebugFlags called on", c.Name()) + var debugflags func(*Command) + + debugflags = func(x *Command) { + if x.HasFlags() || x.HasPersistentFlags() { + c.Println(x.Name()) + } + if x.HasFlags() { + x.flags.VisitAll(func(f *flag.Flag) { + if x.HasPersistentFlags() { + if x.persistentFlag(f.Name) == nil { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [L]") + } else { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [LP]") + } + } else { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [L]") + } + }) + } + if x.HasPersistentFlags() { + x.pflags.VisitAll(func(f *flag.Flag) { + if x.HasFlags() { + if x.flags.Lookup(f.Name) == nil { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [P]") + } + } else { + c.Println(" -"+f.Shorthand+",", "--"+f.Name, "["+f.DefValue+"]", "", f.Value, " [P]") + } + }) + } + c.Println(x.flagErrorBuf) + if x.HasSubCommands() { + for _, y := range x.commands { + debugflags(y) + } + } + } + + debugflags(c) +} + +// Name returns the command's name: the first word in the use line. +func (c *Command) Name() string { + if c.name != "" { + return c.name + } + name := c.Use + i := strings.Index(name, " ") + if i >= 0 { + name = name[:i] + } + c.name = name + return c.name +} + +// HasAlias determines if a given string is an alias of the command. +func (c *Command) HasAlias(s string) bool { + for _, a := range c.Aliases { + if a == s { + return true + } + } + return false +} + +// NameAndAliases returns string containing name and all aliases +func (c *Command) NameAndAliases() string { + return strings.Join(append([]string{c.Name()}, c.Aliases...), ", ") +} + +// HasExample determines if the command has example. +func (c *Command) HasExample() bool { + return len(c.Example) > 0 +} + +// Runnable determines if the command is itself runnable. +func (c *Command) Runnable() bool { + return c.Run != nil || c.RunE != nil +} + +// HasSubCommands determines if the command has children commands. +func (c *Command) HasSubCommands() bool { + return len(c.commands) > 0 +} + +// IsAvailableCommand determines if a command is available as a non-help command +// (this includes all non deprecated/hidden commands). +func (c *Command) IsAvailableCommand() bool { + if len(c.Deprecated) != 0 || c.Hidden { + return false + } + + if c.HasParent() && c.Parent().helpCommand == c { + return false + } + + if c.Runnable() || c.HasAvailableSubCommands() { + return true + } + + return false +} + +// IsHelpCommand determines if a command is a 'help' command; a help command is +// determined by the fact that it is NOT runnable/hidden/deprecated, and has no +// sub commands that are runnable/hidden/deprecated. +func (c *Command) IsHelpCommand() bool { + + // if a command is runnable, deprecated, or hidden it is not a 'help' command + if c.Runnable() || len(c.Deprecated) != 0 || c.Hidden { + return false + } + + // if any non-help sub commands are found, the command is not a 'help' command + for _, sub := range c.commands { + if !sub.IsHelpCommand() { + return false + } + } + + // the command either has no sub commands, or no non-help sub commands + return true +} + +// HasHelpSubCommands determines if a command has any available 'help' sub commands +// that need to be shown in the usage/help default template under 'additional help +// topics'. +func (c *Command) HasHelpSubCommands() bool { + + // return true on the first found available 'help' sub command + for _, sub := range c.commands { + if sub.IsHelpCommand() { + return true + } + } + + // the command either has no sub commands, or no available 'help' sub commands + return false +} + +// HasAvailableSubCommands determines if a command has available sub commands that +// need to be shown in the usage/help default template under 'available commands'. +func (c *Command) HasAvailableSubCommands() bool { + + // return true on the first found available (non deprecated/help/hidden) + // sub command + for _, sub := range c.commands { + if sub.IsAvailableCommand() { + return true + } + } + + // the command either has no sub comamnds, or no available (non deprecated/help/hidden) + // sub commands + return false +} + +// HasParent determines if the command is a child command. +func (c *Command) HasParent() bool { + return c.parent != nil +} + +// GlobalNormalizationFunc returns the global normalization function or nil if doesn't exists. +func (c *Command) GlobalNormalizationFunc() func(f *flag.FlagSet, name string) flag.NormalizedName { + return c.globNormFunc +} + +// Flags returns the complete FlagSet that applies +// to this command (local and persistent declared here and by all parents). +func (c *Command) Flags() *flag.FlagSet { + if c.flags == nil { + c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + c.flags.SetOutput(c.flagErrorBuf) + } + return c.flags +} + +// LocalNonPersistentFlags are flags specific to this command which will NOT persist to subcommands. +func (c *Command) LocalNonPersistentFlags() *flag.FlagSet { + persistentFlags := c.PersistentFlags() + + out := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.LocalFlags().VisitAll(func(f *flag.Flag) { + if persistentFlags.Lookup(f.Name) == nil { + out.AddFlag(f) + } + }) + return out +} + +// LocalFlags returns the local FlagSet specifically set in the current command. +func (c *Command) LocalFlags() *flag.FlagSet { + c.mergePersistentFlags() + + local := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.lflags.VisitAll(func(f *flag.Flag) { + local.AddFlag(f) + }) + if !c.HasParent() { + flag.CommandLine.VisitAll(func(f *flag.Flag) { + if local.Lookup(f.Name) == nil { + local.AddFlag(f) + } + }) + } + return local +} + +// InheritedFlags returns all flags which were inherited from parents commands. +func (c *Command) InheritedFlags() *flag.FlagSet { + c.mergePersistentFlags() + + inherited := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + local := c.LocalFlags() + + var rmerge func(x *Command) + + rmerge = func(x *Command) { + if x.HasPersistentFlags() { + x.PersistentFlags().VisitAll(func(f *flag.Flag) { + if inherited.Lookup(f.Name) == nil && local.Lookup(f.Name) == nil { + inherited.AddFlag(f) + } + }) + } + if x.HasParent() { + rmerge(x.parent) + } + } + + if c.HasParent() { + rmerge(c.parent) + } + + return inherited +} + +// NonInheritedFlags returns all flags which were not inherited from parent commands. +func (c *Command) NonInheritedFlags() *flag.FlagSet { + return c.LocalFlags() +} + +// PersistentFlags returns the persistent FlagSet specifically set in the current command. +func (c *Command) PersistentFlags() *flag.FlagSet { + if c.pflags == nil { + c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + c.pflags.SetOutput(c.flagErrorBuf) + } + return c.pflags +} + +// ResetFlags is used in testing. +func (c *Command) ResetFlags() { + c.flagErrorBuf = new(bytes.Buffer) + c.flagErrorBuf.Reset() + c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.flags.SetOutput(c.flagErrorBuf) + c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.pflags.SetOutput(c.flagErrorBuf) +} + +// HasFlags checks if the command contains any flags (local plus persistent from the entire structure). +func (c *Command) HasFlags() bool { + return c.Flags().HasFlags() +} + +// HasPersistentFlags checks if the command contains persistent flags. +func (c *Command) HasPersistentFlags() bool { + return c.PersistentFlags().HasFlags() +} + +// HasLocalFlags checks if the command has flags specifically declared locally. +func (c *Command) HasLocalFlags() bool { + return c.LocalFlags().HasFlags() +} + +// HasInheritedFlags checks if the command has flags inherited from its parent command. +func (c *Command) HasInheritedFlags() bool { + return c.InheritedFlags().HasFlags() +} + +// HasAvailableFlags checks if the command contains any flags (local plus persistent from the entire +// structure) which are not hidden or deprecated. +func (c *Command) HasAvailableFlags() bool { + return c.Flags().HasAvailableFlags() +} + +// HasAvailablePersistentFlags checks if the command contains persistent flags which are not hidden or deprecated. +func (c *Command) HasAvailablePersistentFlags() bool { + return c.PersistentFlags().HasAvailableFlags() +} + +// HasAvailableLocalFlags checks if the command has flags specifically declared locally which are not hidden +// or deprecated. +func (c *Command) HasAvailableLocalFlags() bool { + return c.LocalFlags().HasAvailableFlags() +} + +// HasAvailableInheritedFlags checks if the command has flags inherited from its parent command which are +// not hidden or deprecated. +func (c *Command) HasAvailableInheritedFlags() bool { + return c.InheritedFlags().HasAvailableFlags() +} + +// Flag climbs up the command tree looking for matching flag. +func (c *Command) Flag(name string) (flag *flag.Flag) { + flag = c.Flags().Lookup(name) + + if flag == nil { + flag = c.persistentFlag(name) + } + + return +} + +// Recursively find matching persistent flag. +func (c *Command) persistentFlag(name string) (flag *flag.Flag) { + if c.HasPersistentFlags() { + flag = c.PersistentFlags().Lookup(name) + } + + if flag == nil && c.HasParent() { + flag = c.parent.persistentFlag(name) + } + return +} + +// ParseFlags parses persistent flag tree and local flags. +func (c *Command) ParseFlags(args []string) (err error) { + if c.DisableFlagParsing { + return nil + } + c.mergePersistentFlags() + err = c.Flags().Parse(args) + return +} + +// Parent returns a commands parent command. +func (c *Command) Parent() *Command { + return c.parent +} + +func (c *Command) mergePersistentFlags() { + var rmerge func(x *Command) + + // Save the set of local flags + if c.lflags == nil { + c.lflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + if c.flagErrorBuf == nil { + c.flagErrorBuf = new(bytes.Buffer) + } + c.lflags.SetOutput(c.flagErrorBuf) + addtolocal := func(f *flag.Flag) { + c.lflags.AddFlag(f) + } + c.Flags().VisitAll(addtolocal) + c.PersistentFlags().VisitAll(addtolocal) + } + rmerge = func(x *Command) { + if !x.HasParent() { + flag.CommandLine.VisitAll(func(f *flag.Flag) { + if x.PersistentFlags().Lookup(f.Name) == nil { + x.PersistentFlags().AddFlag(f) + } + }) + } + if x.HasPersistentFlags() { + x.PersistentFlags().VisitAll(func(f *flag.Flag) { + if c.Flags().Lookup(f.Name) == nil { + c.Flags().AddFlag(f) + } + }) + } + if x.HasParent() { + rmerge(x.parent) + } + } + + rmerge(c) +} diff --git a/cmd/bleve/vendor/github.com/spf13/cobra/command_notwin.go b/cmd/bleve/vendor/github.com/spf13/cobra/command_notwin.go new file mode 100644 index 0000000..6159c1c --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/cobra/command_notwin.go @@ -0,0 +1,5 @@ +// +build !windows + +package cobra + +var preExecHookFn func(*Command) diff --git a/cmd/bleve/vendor/github.com/spf13/cobra/command_win.go b/cmd/bleve/vendor/github.com/spf13/cobra/command_win.go new file mode 100644 index 0000000..4b0eaa1 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/cobra/command_win.go @@ -0,0 +1,26 @@ +// +build windows + +package cobra + +import ( + "os" + "time" + + "github.com/inconshreveable/mousetrap" +) + +var preExecHookFn = preExecHook + +// enables an information splash screen on Windows if the CLI is started from explorer.exe. +var MousetrapHelpText string = `This is a command line tool + +You need to open cmd.exe and run it from there. +` + +func preExecHook(c *Command) { + if mousetrap.StartedByExplorer() { + c.Print(MousetrapHelpText) + time.Sleep(5 * time.Second) + os.Exit(1) + } +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/LICENSE b/cmd/bleve/vendor/github.com/spf13/pflag/LICENSE new file mode 100644 index 0000000..63ed1cf --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2012 Alex Ogier. All rights reserved. +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/bool.go b/cmd/bleve/vendor/github.com/spf13/pflag/bool.go new file mode 100644 index 0000000..c4c5c0b --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/bool.go @@ -0,0 +1,94 @@ +package pflag + +import "strconv" + +// optional interface to indicate boolean flags that can be +// supplied without "=value" text +type boolFlag interface { + Value + IsBoolFlag() bool +} + +// -- bool Value +type boolValue bool + +func newBoolValue(val bool, p *bool) *boolValue { + *p = val + return (*boolValue)(p) +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + *b = boolValue(v) + return err +} + +func (b *boolValue) Type() string { + return "bool" +} + +func (b *boolValue) String() string { return strconv.FormatBool(bool(*b)) } + +func (b *boolValue) IsBoolFlag() bool { return true } + +func boolConv(sval string) (interface{}, error) { + return strconv.ParseBool(sval) +} + +// GetBool return the bool value of a flag with the given name +func (f *FlagSet) GetBool(name string) (bool, error) { + val, err := f.getFlagType(name, "bool", boolConv) + if err != nil { + return false, err + } + return val.(bool), nil +} + +// BoolVar defines a bool flag with specified name, default value, and usage string. +// The argument p points to a bool variable in which to store the value of the flag. +func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string) { + f.BoolVarP(p, name, "", value, usage) +} + +// BoolVarP is like BoolVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) BoolVarP(p *bool, name, shorthand string, value bool, usage string) { + flag := f.VarPF(newBoolValue(value, p), name, shorthand, usage) + flag.NoOptDefVal = "true" +} + +// BoolVar defines a bool flag with specified name, default value, and usage string. +// The argument p points to a bool variable in which to store the value of the flag. +func BoolVar(p *bool, name string, value bool, usage string) { + BoolVarP(p, name, "", value, usage) +} + +// BoolVarP is like BoolVar, but accepts a shorthand letter that can be used after a single dash. +func BoolVarP(p *bool, name, shorthand string, value bool, usage string) { + flag := CommandLine.VarPF(newBoolValue(value, p), name, shorthand, usage) + flag.NoOptDefVal = "true" +} + +// Bool defines a bool flag with specified name, default value, and usage string. +// The return value is the address of a bool variable that stores the value of the flag. +func (f *FlagSet) Bool(name string, value bool, usage string) *bool { + return f.BoolP(name, "", value, usage) +} + +// BoolP is like Bool, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) BoolP(name, shorthand string, value bool, usage string) *bool { + p := new(bool) + f.BoolVarP(p, name, shorthand, value, usage) + return p +} + +// Bool defines a bool flag with specified name, default value, and usage string. +// The return value is the address of a bool variable that stores the value of the flag. +func Bool(name string, value bool, usage string) *bool { + return BoolP(name, "", value, usage) +} + +// BoolP is like Bool, but accepts a shorthand letter that can be used after a single dash. +func BoolP(name, shorthand string, value bool, usage string) *bool { + b := CommandLine.BoolP(name, shorthand, value, usage) + return b +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/bool_slice.go b/cmd/bleve/vendor/github.com/spf13/pflag/bool_slice.go new file mode 100644 index 0000000..5af02f1 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/bool_slice.go @@ -0,0 +1,147 @@ +package pflag + +import ( + "io" + "strconv" + "strings" +) + +// -- boolSlice Value +type boolSliceValue struct { + value *[]bool + changed bool +} + +func newBoolSliceValue(val []bool, p *[]bool) *boolSliceValue { + bsv := new(boolSliceValue) + bsv.value = p + *bsv.value = val + return bsv +} + +// Set converts, and assigns, the comma-separated boolean argument string representation as the []bool value of this flag. +// If Set is called on a flag that already has a []bool assigned, the newly converted values will be appended. +func (s *boolSliceValue) Set(val string) error { + + // remove all quote characters + rmQuote := strings.NewReplacer(`"`, "", `'`, "", "`", "") + + // read flag arguments with CSV parser + boolStrSlice, err := readAsCSV(rmQuote.Replace(val)) + if err != nil && err != io.EOF { + return err + } + + // parse boolean values into slice + out := make([]bool, 0, len(boolStrSlice)) + for _, boolStr := range boolStrSlice { + b, err := strconv.ParseBool(strings.TrimSpace(boolStr)) + if err != nil { + return err + } + out = append(out, b) + } + + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + + s.changed = true + + return nil +} + +// Type returns a string that uniquely represents this flag's type. +func (s *boolSliceValue) Type() string { + return "boolSlice" +} + +// String defines a "native" format for this boolean slice flag value. +func (s *boolSliceValue) String() string { + + boolStrSlice := make([]string, len(*s.value)) + for i, b := range *s.value { + boolStrSlice[i] = strconv.FormatBool(b) + } + + out, _ := writeAsCSV(boolStrSlice) + + return "[" + out + "]" +} + +func boolSliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Empty string would cause a slice with one (empty) entry + if len(val) == 0 { + return []bool{}, nil + } + ss := strings.Split(val, ",") + out := make([]bool, len(ss)) + for i, t := range ss { + var err error + out[i], err = strconv.ParseBool(t) + if err != nil { + return nil, err + } + } + return out, nil +} + +// GetBoolSlice returns the []bool value of a flag with the given name. +func (f *FlagSet) GetBoolSlice(name string) ([]bool, error) { + val, err := f.getFlagType(name, "boolSlice", boolSliceConv) + if err != nil { + return []bool{}, err + } + return val.([]bool), nil +} + +// BoolSliceVar defines a boolSlice flag with specified name, default value, and usage string. +// The argument p points to a []bool variable in which to store the value of the flag. +func (f *FlagSet) BoolSliceVar(p *[]bool, name string, value []bool, usage string) { + f.VarP(newBoolSliceValue(value, p), name, "", usage) +} + +// BoolSliceVarP is like BoolSliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) BoolSliceVarP(p *[]bool, name, shorthand string, value []bool, usage string) { + f.VarP(newBoolSliceValue(value, p), name, shorthand, usage) +} + +// BoolSliceVar defines a []bool flag with specified name, default value, and usage string. +// The argument p points to a []bool variable in which to store the value of the flag. +func BoolSliceVar(p *[]bool, name string, value []bool, usage string) { + CommandLine.VarP(newBoolSliceValue(value, p), name, "", usage) +} + +// BoolSliceVarP is like BoolSliceVar, but accepts a shorthand letter that can be used after a single dash. +func BoolSliceVarP(p *[]bool, name, shorthand string, value []bool, usage string) { + CommandLine.VarP(newBoolSliceValue(value, p), name, shorthand, usage) +} + +// BoolSlice defines a []bool flag with specified name, default value, and usage string. +// The return value is the address of a []bool variable that stores the value of the flag. +func (f *FlagSet) BoolSlice(name string, value []bool, usage string) *[]bool { + p := []bool{} + f.BoolSliceVarP(&p, name, "", value, usage) + return &p +} + +// BoolSliceP is like BoolSlice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) BoolSliceP(name, shorthand string, value []bool, usage string) *[]bool { + p := []bool{} + f.BoolSliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// BoolSlice defines a []bool flag with specified name, default value, and usage string. +// The return value is the address of a []bool variable that stores the value of the flag. +func BoolSlice(name string, value []bool, usage string) *[]bool { + return CommandLine.BoolSliceP(name, "", value, usage) +} + +// BoolSliceP is like BoolSlice, but accepts a shorthand letter that can be used after a single dash. +func BoolSliceP(name, shorthand string, value []bool, usage string) *[]bool { + return CommandLine.BoolSliceP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/count.go b/cmd/bleve/vendor/github.com/spf13/pflag/count.go new file mode 100644 index 0000000..d22be41 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/count.go @@ -0,0 +1,94 @@ +package pflag + +import "strconv" + +// -- count Value +type countValue int + +func newCountValue(val int, p *int) *countValue { + *p = val + return (*countValue)(p) +} + +func (i *countValue) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + // -1 means that no specific value was passed, so increment + if v == -1 { + *i = countValue(*i + 1) + } else { + *i = countValue(v) + } + return err +} + +func (i *countValue) Type() string { + return "count" +} + +func (i *countValue) String() string { return strconv.Itoa(int(*i)) } + +func countConv(sval string) (interface{}, error) { + i, err := strconv.Atoi(sval) + if err != nil { + return nil, err + } + return i, nil +} + +// GetCount return the int value of a flag with the given name +func (f *FlagSet) GetCount(name string) (int, error) { + val, err := f.getFlagType(name, "count", countConv) + if err != nil { + return 0, err + } + return val.(int), nil +} + +// CountVar defines a count flag with specified name, default value, and usage string. +// The argument p points to an int variable in which to store the value of the flag. +// A count flag will add 1 to its value evey time it is found on the command line +func (f *FlagSet) CountVar(p *int, name string, usage string) { + f.CountVarP(p, name, "", usage) +} + +// CountVarP is like CountVar only take a shorthand for the flag name. +func (f *FlagSet) CountVarP(p *int, name, shorthand string, usage string) { + flag := f.VarPF(newCountValue(0, p), name, shorthand, usage) + flag.NoOptDefVal = "-1" +} + +// CountVar like CountVar only the flag is placed on the CommandLine instead of a given flag set +func CountVar(p *int, name string, usage string) { + CommandLine.CountVar(p, name, usage) +} + +// CountVarP is like CountVar only take a shorthand for the flag name. +func CountVarP(p *int, name, shorthand string, usage string) { + CommandLine.CountVarP(p, name, shorthand, usage) +} + +// Count defines a count flag with specified name, default value, and usage string. +// The return value is the address of an int variable that stores the value of the flag. +// A count flag will add 1 to its value evey time it is found on the command line +func (f *FlagSet) Count(name string, usage string) *int { + p := new(int) + f.CountVarP(p, name, "", usage) + return p +} + +// CountP is like Count only takes a shorthand for the flag name. +func (f *FlagSet) CountP(name, shorthand string, usage string) *int { + p := new(int) + f.CountVarP(p, name, shorthand, usage) + return p +} + +// Count like Count only the flag is placed on the CommandLine isntead of a given flag set +func Count(name string, usage string) *int { + return CommandLine.CountP(name, "", usage) +} + +// CountP is like Count only takes a shorthand for the flag name. +func CountP(name, shorthand string, usage string) *int { + return CommandLine.CountP(name, shorthand, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/duration.go b/cmd/bleve/vendor/github.com/spf13/pflag/duration.go new file mode 100644 index 0000000..e9debef --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/duration.go @@ -0,0 +1,86 @@ +package pflag + +import ( + "time" +) + +// -- time.Duration Value +type durationValue time.Duration + +func newDurationValue(val time.Duration, p *time.Duration) *durationValue { + *p = val + return (*durationValue)(p) +} + +func (d *durationValue) Set(s string) error { + v, err := time.ParseDuration(s) + *d = durationValue(v) + return err +} + +func (d *durationValue) Type() string { + return "duration" +} + +func (d *durationValue) String() string { return (*time.Duration)(d).String() } + +func durationConv(sval string) (interface{}, error) { + return time.ParseDuration(sval) +} + +// GetDuration return the duration value of a flag with the given name +func (f *FlagSet) GetDuration(name string) (time.Duration, error) { + val, err := f.getFlagType(name, "duration", durationConv) + if err != nil { + return 0, err + } + return val.(time.Duration), nil +} + +// DurationVar defines a time.Duration flag with specified name, default value, and usage string. +// The argument p points to a time.Duration variable in which to store the value of the flag. +func (f *FlagSet) DurationVar(p *time.Duration, name string, value time.Duration, usage string) { + f.VarP(newDurationValue(value, p), name, "", usage) +} + +// DurationVarP is like DurationVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) DurationVarP(p *time.Duration, name, shorthand string, value time.Duration, usage string) { + f.VarP(newDurationValue(value, p), name, shorthand, usage) +} + +// DurationVar defines a time.Duration flag with specified name, default value, and usage string. +// The argument p points to a time.Duration variable in which to store the value of the flag. +func DurationVar(p *time.Duration, name string, value time.Duration, usage string) { + CommandLine.VarP(newDurationValue(value, p), name, "", usage) +} + +// DurationVarP is like DurationVar, but accepts a shorthand letter that can be used after a single dash. +func DurationVarP(p *time.Duration, name, shorthand string, value time.Duration, usage string) { + CommandLine.VarP(newDurationValue(value, p), name, shorthand, usage) +} + +// Duration defines a time.Duration flag with specified name, default value, and usage string. +// The return value is the address of a time.Duration variable that stores the value of the flag. +func (f *FlagSet) Duration(name string, value time.Duration, usage string) *time.Duration { + p := new(time.Duration) + f.DurationVarP(p, name, "", value, usage) + return p +} + +// DurationP is like Duration, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) DurationP(name, shorthand string, value time.Duration, usage string) *time.Duration { + p := new(time.Duration) + f.DurationVarP(p, name, shorthand, value, usage) + return p +} + +// Duration defines a time.Duration flag with specified name, default value, and usage string. +// The return value is the address of a time.Duration variable that stores the value of the flag. +func Duration(name string, value time.Duration, usage string) *time.Duration { + return CommandLine.DurationP(name, "", value, usage) +} + +// DurationP is like Duration, but accepts a shorthand letter that can be used after a single dash. +func DurationP(name, shorthand string, value time.Duration, usage string) *time.Duration { + return CommandLine.DurationP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/flag.go b/cmd/bleve/vendor/github.com/spf13/pflag/flag.go new file mode 100644 index 0000000..746af63 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/flag.go @@ -0,0 +1,1063 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package pflag is a drop-in replacement for Go's flag package, implementing +POSIX/GNU-style --flags. + +pflag is compatible with the GNU extensions to the POSIX recommendations +for command-line options. See +http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html + +Usage: + +pflag is a drop-in replacement of Go's native flag package. If you import +pflag under the name "flag" then all code should continue to function +with no changes. + + import flag "github.com/ogier/pflag" + + There is one exception to this: if you directly instantiate the Flag struct +there is one more field "Shorthand" that you will need to set. +Most code never instantiates this struct directly, and instead uses +functions such as String(), BoolVar(), and Var(), and is therefore +unaffected. + +Define flags using flag.String(), Bool(), Int(), etc. + +This declares an integer flag, -flagname, stored in the pointer ip, with type *int. + var ip = flag.Int("flagname", 1234, "help message for flagname") +If you like, you can bind the flag to a variable using the Var() functions. + var flagvar int + func init() { + flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname") + } +Or you can create custom flags that satisfy the Value interface (with +pointer receivers) and couple them to flag parsing by + flag.Var(&flagVal, "name", "help message for flagname") +For such flags, the default value is just the initial value of the variable. + +After all flags are defined, call + flag.Parse() +to parse the command line into the defined flags. + +Flags may then be used directly. If you're using the flags themselves, +they are all pointers; if you bind to variables, they're values. + fmt.Println("ip has value ", *ip) + fmt.Println("flagvar has value ", flagvar) + +After parsing, the arguments after the flag are available as the +slice flag.Args() or individually as flag.Arg(i). +The arguments are indexed from 0 through flag.NArg()-1. + +The pflag package also defines some new functions that are not in flag, +that give one-letter shorthands for flags. You can use these by appending +'P' to the name of any function that defines a flag. + var ip = flag.IntP("flagname", "f", 1234, "help message") + var flagvar bool + func init() { + flag.BoolVarP("boolname", "b", true, "help message") + } + flag.VarP(&flagVar, "varname", "v", 1234, "help message") +Shorthand letters can be used with single dashes on the command line. +Boolean shorthand flags can be combined with other shorthand flags. + +Command line flag syntax: + --flag // boolean flags only + --flag=x + +Unlike the flag package, a single dash before an option means something +different than a double dash. Single dashes signify a series of shorthand +letters for flags. All but the last shorthand letter must be boolean flags. + // boolean flags + -f + -abc + // non-boolean flags + -n 1234 + -Ifile + // mixed + -abcs "hello" + -abcn1234 + +Flag parsing stops after the terminator "--". Unlike the flag package, +flags can be interspersed with arguments anywhere on the command line +before this terminator. + +Integer flags accept 1234, 0664, 0x1234 and may be negative. +Boolean flags (in their long form) accept 1, 0, t, f, true, false, +TRUE, FALSE, True, False. +Duration flags accept any input valid for time.ParseDuration. + +The default set of command-line flags is controlled by +top-level functions. The FlagSet type allows one to define +independent sets of flags, such as to implement subcommands +in a command-line interface. The methods of FlagSet are +analogous to the top-level functions for the command-line +flag set. +*/ +package pflag + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "sort" + "strings" +) + +// ErrHelp is the error returned if the flag -help is invoked but no such flag is defined. +var ErrHelp = errors.New("pflag: help requested") + +// ErrorHandling defines how to handle flag parsing errors. +type ErrorHandling int + +const ( + // ContinueOnError will return an err from Parse() if an error is found + ContinueOnError ErrorHandling = iota + // ExitOnError will call os.Exit(2) if an error is found when parsing + ExitOnError + // PanicOnError will panic() if an error is found when parsing flags + PanicOnError +) + +// NormalizedName is a flag name that has been normalized according to rules +// for the FlagSet (e.g. making '-' and '_' equivalent). +type NormalizedName string + +// A FlagSet represents a set of defined flags. +type FlagSet struct { + // Usage is the function called when an error occurs while parsing flags. + // The field is a function (not a method) that may be changed to point to + // a custom error handler. + Usage func() + + name string + parsed bool + actual map[NormalizedName]*Flag + formal map[NormalizedName]*Flag + shorthands map[byte]*Flag + args []string // arguments after flags + argsLenAtDash int // len(args) when a '--' was located when parsing, or -1 if no -- + exitOnError bool // does the program exit if there's an error? + errorHandling ErrorHandling + output io.Writer // nil means stderr; use out() accessor + interspersed bool // allow interspersed option/non-option args + normalizeNameFunc func(f *FlagSet, name string) NormalizedName +} + +// A Flag represents the state of a flag. +type Flag struct { + Name string // name as it appears on command line + Shorthand string // one-letter abbreviated flag + Usage string // help message + Value Value // value as set + DefValue string // default value (as text); for usage message + Changed bool // If the user set the value (or if left to default) + NoOptDefVal string //default value (as text); if the flag is on the command line without any options + Deprecated string // If this flag is deprecated, this string is the new or now thing to use + Hidden bool // used by cobra.Command to allow flags to be hidden from help/usage text + ShorthandDeprecated string // If the shorthand of this flag is deprecated, this string is the new or now thing to use + Annotations map[string][]string // used by cobra.Command bash autocomple code +} + +// Value is the interface to the dynamic value stored in a flag. +// (The default value is represented as a string.) +type Value interface { + String() string + Set(string) error + Type() string +} + +// sortFlags returns the flags as a slice in lexicographical sorted order. +func sortFlags(flags map[NormalizedName]*Flag) []*Flag { + list := make(sort.StringSlice, len(flags)) + i := 0 + for k := range flags { + list[i] = string(k) + i++ + } + list.Sort() + result := make([]*Flag, len(list)) + for i, name := range list { + result[i] = flags[NormalizedName(name)] + } + return result +} + +// SetNormalizeFunc allows you to add a function which can translate flag names. +// Flags added to the FlagSet will be translated and then when anything tries to +// look up the flag that will also be translated. So it would be possible to create +// a flag named "getURL" and have it translated to "geturl". A user could then pass +// "--getUrl" which may also be translated to "geturl" and everything will work. +func (f *FlagSet) SetNormalizeFunc(n func(f *FlagSet, name string) NormalizedName) { + f.normalizeNameFunc = n + for k, v := range f.formal { + delete(f.formal, k) + nname := f.normalizeFlagName(string(k)) + f.formal[nname] = v + v.Name = string(nname) + } +} + +// GetNormalizeFunc returns the previously set NormalizeFunc of a function which +// does no translation, if not set previously. +func (f *FlagSet) GetNormalizeFunc() func(f *FlagSet, name string) NormalizedName { + if f.normalizeNameFunc != nil { + return f.normalizeNameFunc + } + return func(f *FlagSet, name string) NormalizedName { return NormalizedName(name) } +} + +func (f *FlagSet) normalizeFlagName(name string) NormalizedName { + n := f.GetNormalizeFunc() + return n(f, name) +} + +func (f *FlagSet) out() io.Writer { + if f.output == nil { + return os.Stderr + } + return f.output +} + +// SetOutput sets the destination for usage and error messages. +// If output is nil, os.Stderr is used. +func (f *FlagSet) SetOutput(output io.Writer) { + f.output = output +} + +// VisitAll visits the flags in lexicographical order, calling fn for each. +// It visits all flags, even those not set. +func (f *FlagSet) VisitAll(fn func(*Flag)) { + for _, flag := range sortFlags(f.formal) { + fn(flag) + } +} + +// HasFlags returns a bool to indicate if the FlagSet has any flags definied. +func (f *FlagSet) HasFlags() bool { + return len(f.formal) > 0 +} + +// HasAvailableFlags returns a bool to indicate if the FlagSet has any flags +// definied that are not hidden or deprecated. +func (f *FlagSet) HasAvailableFlags() bool { + for _, flag := range f.formal { + if !flag.Hidden && len(flag.Deprecated) == 0 { + return true + } + } + return false +} + +// VisitAll visits the command-line flags in lexicographical order, calling +// fn for each. It visits all flags, even those not set. +func VisitAll(fn func(*Flag)) { + CommandLine.VisitAll(fn) +} + +// Visit visits the flags in lexicographical order, calling fn for each. +// It visits only those flags that have been set. +func (f *FlagSet) Visit(fn func(*Flag)) { + for _, flag := range sortFlags(f.actual) { + fn(flag) + } +} + +// Visit visits the command-line flags in lexicographical order, calling fn +// for each. It visits only those flags that have been set. +func Visit(fn func(*Flag)) { + CommandLine.Visit(fn) +} + +// Lookup returns the Flag structure of the named flag, returning nil if none exists. +func (f *FlagSet) Lookup(name string) *Flag { + return f.lookup(f.normalizeFlagName(name)) +} + +// lookup returns the Flag structure of the named flag, returning nil if none exists. +func (f *FlagSet) lookup(name NormalizedName) *Flag { + return f.formal[name] +} + +// func to return a given type for a given flag name +func (f *FlagSet) getFlagType(name string, ftype string, convFunc func(sval string) (interface{}, error)) (interface{}, error) { + flag := f.Lookup(name) + if flag == nil { + err := fmt.Errorf("flag accessed but not defined: %s", name) + return nil, err + } + + if flag.Value.Type() != ftype { + err := fmt.Errorf("trying to get %s value of flag of type %s", ftype, flag.Value.Type()) + return nil, err + } + + sval := flag.Value.String() + result, err := convFunc(sval) + if err != nil { + return nil, err + } + return result, nil +} + +// ArgsLenAtDash will return the length of f.Args at the moment when a -- was +// found during arg parsing. This allows your program to know which args were +// before the -- and which came after. +func (f *FlagSet) ArgsLenAtDash() int { + return f.argsLenAtDash +} + +// MarkDeprecated indicated that a flag is deprecated in your program. It will +// continue to function but will not show up in help or usage messages. Using +// this flag will also print the given usageMessage. +func (f *FlagSet) MarkDeprecated(name string, usageMessage string) error { + flag := f.Lookup(name) + if flag == nil { + return fmt.Errorf("flag %q does not exist", name) + } + if len(usageMessage) == 0 { + return fmt.Errorf("deprecated message for flag %q must be set", name) + } + flag.Deprecated = usageMessage + return nil +} + +// MarkShorthandDeprecated will mark the shorthand of a flag deprecated in your +// program. It will continue to function but will not show up in help or usage +// messages. Using this flag will also print the given usageMessage. +func (f *FlagSet) MarkShorthandDeprecated(name string, usageMessage string) error { + flag := f.Lookup(name) + if flag == nil { + return fmt.Errorf("flag %q does not exist", name) + } + if len(usageMessage) == 0 { + return fmt.Errorf("deprecated message for flag %q must be set", name) + } + flag.ShorthandDeprecated = usageMessage + return nil +} + +// MarkHidden sets a flag to 'hidden' in your program. It will continue to +// function but will not show up in help or usage messages. +func (f *FlagSet) MarkHidden(name string) error { + flag := f.Lookup(name) + if flag == nil { + return fmt.Errorf("flag %q does not exist", name) + } + flag.Hidden = true + return nil +} + +// Lookup returns the Flag structure of the named command-line flag, +// returning nil if none exists. +func Lookup(name string) *Flag { + return CommandLine.Lookup(name) +} + +// Set sets the value of the named flag. +func (f *FlagSet) Set(name, value string) error { + normalName := f.normalizeFlagName(name) + flag, ok := f.formal[normalName] + if !ok { + return fmt.Errorf("no such flag -%v", name) + } + err := flag.Value.Set(value) + if err != nil { + return err + } + if f.actual == nil { + f.actual = make(map[NormalizedName]*Flag) + } + f.actual[normalName] = flag + flag.Changed = true + if len(flag.Deprecated) > 0 { + fmt.Fprintf(os.Stderr, "Flag --%s has been deprecated, %s\n", flag.Name, flag.Deprecated) + } + return nil +} + +// SetAnnotation allows one to set arbitrary annotations on a flag in the FlagSet. +// This is sometimes used by spf13/cobra programs which want to generate additional +// bash completion information. +func (f *FlagSet) SetAnnotation(name, key string, values []string) error { + normalName := f.normalizeFlagName(name) + flag, ok := f.formal[normalName] + if !ok { + return fmt.Errorf("no such flag -%v", name) + } + if flag.Annotations == nil { + flag.Annotations = map[string][]string{} + } + flag.Annotations[key] = values + return nil +} + +// Changed returns true if the flag was explicitly set during Parse() and false +// otherwise +func (f *FlagSet) Changed(name string) bool { + flag := f.Lookup(name) + // If a flag doesn't exist, it wasn't changed.... + if flag == nil { + return false + } + return flag.Changed +} + +// Set sets the value of the named command-line flag. +func Set(name, value string) error { + return CommandLine.Set(name, value) +} + +// PrintDefaults prints, to standard error unless configured +// otherwise, the default values of all defined flags in the set. +func (f *FlagSet) PrintDefaults() { + usages := f.FlagUsages() + fmt.Fprint(f.out(), usages) +} + +// defaultIsZeroValue returns true if the default value for this flag represents +// a zero value. +func (f *Flag) defaultIsZeroValue() bool { + switch f.Value.(type) { + case boolFlag: + return f.DefValue == "false" + case *durationValue: + // Beginning in Go 1.7, duration zero values are "0s" + return f.DefValue == "0" || f.DefValue == "0s" + case *intValue, *int8Value, *int32Value, *int64Value, *uintValue, *uint8Value, *uint16Value, *uint32Value, *uint64Value, *countValue, *float32Value, *float64Value: + return f.DefValue == "0" + case *stringValue: + return f.DefValue == "" + case *ipValue, *ipMaskValue, *ipNetValue: + return f.DefValue == "" + case *intSliceValue, *stringSliceValue, *stringArrayValue: + return f.DefValue == "[]" + default: + switch f.Value.String() { + case "false": + return true + case "": + return true + case "": + return true + case "0": + return true + } + return false + } +} + +// UnquoteUsage extracts a back-quoted name from the usage +// string for a flag and returns it and the un-quoted usage. +// Given "a `name` to show" it returns ("name", "a name to show"). +// If there are no back quotes, the name is an educated guess of the +// type of the flag's value, or the empty string if the flag is boolean. +func UnquoteUsage(flag *Flag) (name string, usage string) { + // Look for a back-quoted name, but avoid the strings package. + usage = flag.Usage + for i := 0; i < len(usage); i++ { + if usage[i] == '`' { + for j := i + 1; j < len(usage); j++ { + if usage[j] == '`' { + name = usage[i+1 : j] + usage = usage[:i] + name + usage[j+1:] + return name, usage + } + } + break // Only one back quote; use type name. + } + } + + name = flag.Value.Type() + switch name { + case "bool": + name = "" + case "float64": + name = "float" + case "int64": + name = "int" + case "uint64": + name = "uint" + } + + return +} + +// Splits the string `s` on whitespace into an initial substring up to +// `i` runes in length and the remainder. Will go `slop` over `i` if +// that encompasses the entire string (which allows the caller to +// avoid short orphan words on the final line). +func wrapN(i, slop int, s string) (string, string) { + if i+slop > len(s) { + return s, "" + } + + w := strings.LastIndexAny(s[:i], " \t") + if w <= 0 { + return s, "" + } + + return s[:w], s[w+1:] +} + +// Wraps the string `s` to a maximum width `w` with leading indent +// `i`. The first line is not indented (this is assumed to be done by +// caller). Pass `w` == 0 to do no wrapping +func wrap(i, w int, s string) string { + if w == 0 { + return s + } + + // space between indent i and end of line width w into which + // we should wrap the text. + wrap := w - i + + var r, l string + + // Not enough space for sensible wrapping. Wrap as a block on + // the next line instead. + if wrap < 24 { + i = 16 + wrap = w - i + r += "\n" + strings.Repeat(" ", i) + } + // If still not enough space then don't even try to wrap. + if wrap < 24 { + return s + } + + // Try to avoid short orphan words on the final line, by + // allowing wrapN to go a bit over if that would fit in the + // remainder of the line. + slop := 5 + wrap = wrap - slop + + // Handle first line, which is indented by the caller (or the + // special case above) + l, s = wrapN(wrap, slop, s) + r = r + l + + // Now wrap the rest + for s != "" { + var t string + + t, s = wrapN(wrap, slop, s) + r = r + "\n" + strings.Repeat(" ", i) + t + } + + return r + +} + +// FlagUsagesWrapped returns a string containing the usage information +// for all flags in the FlagSet. Wrapped to `cols` columns (0 for no +// wrapping) +func (f *FlagSet) FlagUsagesWrapped(cols int) string { + x := new(bytes.Buffer) + + lines := make([]string, 0, len(f.formal)) + + maxlen := 0 + f.VisitAll(func(flag *Flag) { + if len(flag.Deprecated) > 0 || flag.Hidden { + return + } + + line := "" + if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 { + line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name) + } else { + line = fmt.Sprintf(" --%s", flag.Name) + } + + varname, usage := UnquoteUsage(flag) + if len(varname) > 0 { + line += " " + varname + } + if len(flag.NoOptDefVal) > 0 { + switch flag.Value.Type() { + case "string": + line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal) + case "bool": + if flag.NoOptDefVal != "true" { + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + default: + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + } + + // This special character will be replaced with spacing once the + // correct alignment is calculated + line += "\x00" + if len(line) > maxlen { + maxlen = len(line) + } + + line += usage + if !flag.defaultIsZeroValue() { + if flag.Value.Type() == "string" { + line += fmt.Sprintf(" (default \"%s\")", flag.DefValue) + } else { + line += fmt.Sprintf(" (default %s)", flag.DefValue) + } + } + + lines = append(lines, line) + }) + + for _, line := range lines { + sidx := strings.Index(line, "\x00") + spacing := strings.Repeat(" ", maxlen-sidx) + // maxlen + 2 comes from + 1 for the \x00 and + 1 for the (deliberate) off-by-one in maxlen-sidx + fmt.Fprintln(x, line[:sidx], spacing, wrap(maxlen+2, cols, line[sidx+1:])) + } + + return x.String() +} + +// FlagUsages returns a string containing the usage information for all flags in +// the FlagSet +func (f *FlagSet) FlagUsages() string { + return f.FlagUsagesWrapped(0) +} + +// PrintDefaults prints to standard error the default values of all defined command-line flags. +func PrintDefaults() { + CommandLine.PrintDefaults() +} + +// defaultUsage is the default function to print a usage message. +func defaultUsage(f *FlagSet) { + fmt.Fprintf(f.out(), "Usage of %s:\n", f.name) + f.PrintDefaults() +} + +// NOTE: Usage is not just defaultUsage(CommandLine) +// because it serves (via godoc flag Usage) as the example +// for how to write your own usage function. + +// Usage prints to standard error a usage message documenting all defined command-line flags. +// The function is a variable that may be changed to point to a custom function. +// By default it prints a simple header and calls PrintDefaults; for details about the +// format of the output and how to control it, see the documentation for PrintDefaults. +var Usage = func() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + PrintDefaults() +} + +// NFlag returns the number of flags that have been set. +func (f *FlagSet) NFlag() int { return len(f.actual) } + +// NFlag returns the number of command-line flags that have been set. +func NFlag() int { return len(CommandLine.actual) } + +// Arg returns the i'th argument. Arg(0) is the first remaining argument +// after flags have been processed. +func (f *FlagSet) Arg(i int) string { + if i < 0 || i >= len(f.args) { + return "" + } + return f.args[i] +} + +// Arg returns the i'th command-line argument. Arg(0) is the first remaining argument +// after flags have been processed. +func Arg(i int) string { + return CommandLine.Arg(i) +} + +// NArg is the number of arguments remaining after flags have been processed. +func (f *FlagSet) NArg() int { return len(f.args) } + +// NArg is the number of arguments remaining after flags have been processed. +func NArg() int { return len(CommandLine.args) } + +// Args returns the non-flag arguments. +func (f *FlagSet) Args() []string { return f.args } + +// Args returns the non-flag command-line arguments. +func Args() []string { return CommandLine.args } + +// Var defines a flag with the specified name and usage string. The type and +// value of the flag are represented by the first argument, of type Value, which +// typically holds a user-defined implementation of Value. For instance, the +// caller could create a flag that turns a comma-separated string into a slice +// of strings by giving the slice the methods of Value; in particular, Set would +// decompose the comma-separated string into the slice. +func (f *FlagSet) Var(value Value, name string, usage string) { + f.VarP(value, name, "", usage) +} + +// VarPF is like VarP, but returns the flag created +func (f *FlagSet) VarPF(value Value, name, shorthand, usage string) *Flag { + // Remember the default value as a string; it won't change. + flag := &Flag{ + Name: name, + Shorthand: shorthand, + Usage: usage, + Value: value, + DefValue: value.String(), + } + f.AddFlag(flag) + return flag +} + +// VarP is like Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) VarP(value Value, name, shorthand, usage string) { + f.VarPF(value, name, shorthand, usage) +} + +// AddFlag will add the flag to the FlagSet +func (f *FlagSet) AddFlag(flag *Flag) { + // Call normalizeFlagName function only once + normalizedFlagName := f.normalizeFlagName(flag.Name) + + _, alreadythere := f.formal[normalizedFlagName] + if alreadythere { + msg := fmt.Sprintf("%s flag redefined: %s", f.name, flag.Name) + fmt.Fprintln(f.out(), msg) + panic(msg) // Happens only if flags are declared with identical names + } + if f.formal == nil { + f.formal = make(map[NormalizedName]*Flag) + } + + flag.Name = string(normalizedFlagName) + f.formal[normalizedFlagName] = flag + + if len(flag.Shorthand) == 0 { + return + } + if len(flag.Shorthand) > 1 { + fmt.Fprintf(f.out(), "%s shorthand more than ASCII character: %s\n", f.name, flag.Shorthand) + panic("shorthand is more than one character") + } + if f.shorthands == nil { + f.shorthands = make(map[byte]*Flag) + } + c := flag.Shorthand[0] + old, alreadythere := f.shorthands[c] + if alreadythere { + fmt.Fprintf(f.out(), "%s shorthand reused: %q for %s already used for %s\n", f.name, c, flag.Name, old.Name) + panic("shorthand redefinition") + } + f.shorthands[c] = flag +} + +// AddFlagSet adds one FlagSet to another. If a flag is already present in f +// the flag from newSet will be ignored +func (f *FlagSet) AddFlagSet(newSet *FlagSet) { + if newSet == nil { + return + } + newSet.VisitAll(func(flag *Flag) { + if f.Lookup(flag.Name) == nil { + f.AddFlag(flag) + } + }) +} + +// Var defines a flag with the specified name and usage string. The type and +// value of the flag are represented by the first argument, of type Value, which +// typically holds a user-defined implementation of Value. For instance, the +// caller could create a flag that turns a comma-separated string into a slice +// of strings by giving the slice the methods of Value; in particular, Set would +// decompose the comma-separated string into the slice. +func Var(value Value, name string, usage string) { + CommandLine.VarP(value, name, "", usage) +} + +// VarP is like Var, but accepts a shorthand letter that can be used after a single dash. +func VarP(value Value, name, shorthand, usage string) { + CommandLine.VarP(value, name, shorthand, usage) +} + +// failf prints to standard error a formatted error and usage message and +// returns the error. +func (f *FlagSet) failf(format string, a ...interface{}) error { + err := fmt.Errorf(format, a...) + fmt.Fprintln(f.out(), err) + f.usage() + return err +} + +// usage calls the Usage method for the flag set, or the usage function if +// the flag set is CommandLine. +func (f *FlagSet) usage() { + if f == CommandLine { + Usage() + } else if f.Usage == nil { + defaultUsage(f) + } else { + f.Usage() + } +} + +func (f *FlagSet) setFlag(flag *Flag, value string, origArg string) error { + if err := flag.Value.Set(value); err != nil { + return f.failf("invalid argument %q for %s: %v", value, origArg, err) + } + // mark as visited for Visit() + if f.actual == nil { + f.actual = make(map[NormalizedName]*Flag) + } + f.actual[f.normalizeFlagName(flag.Name)] = flag + flag.Changed = true + if len(flag.Deprecated) > 0 { + fmt.Fprintf(os.Stderr, "Flag --%s has been deprecated, %s\n", flag.Name, flag.Deprecated) + } + if len(flag.ShorthandDeprecated) > 0 && containsShorthand(origArg, flag.Shorthand) { + fmt.Fprintf(os.Stderr, "Flag shorthand -%s has been deprecated, %s\n", flag.Shorthand, flag.ShorthandDeprecated) + } + return nil +} + +func containsShorthand(arg, shorthand string) bool { + // filter out flags -- + if strings.HasPrefix(arg, "-") { + return false + } + arg = strings.SplitN(arg, "=", 2)[0] + return strings.Contains(arg, shorthand) +} + +func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []string, err error) { + a = args + name := s[2:] + if len(name) == 0 || name[0] == '-' || name[0] == '=' { + err = f.failf("bad flag syntax: %s", s) + return + } + split := strings.SplitN(name, "=", 2) + name = split[0] + flag, alreadythere := f.formal[f.normalizeFlagName(name)] + if !alreadythere { + if name == "help" { // special case for nice help message. + f.usage() + return a, ErrHelp + } + err = f.failf("unknown flag: --%s", name) + return + } + var value string + if len(split) == 2 { + // '--flag=arg' + value = split[1] + } else if len(flag.NoOptDefVal) > 0 { + // '--flag' (arg was optional) + value = flag.NoOptDefVal + } else if len(a) > 0 { + // '--flag arg' + value = a[0] + a = a[1:] + } else { + // '--flag' (arg was required) + err = f.failf("flag needs an argument: %s", s) + return + } + err = fn(flag, value, s) + return +} + +func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parseFunc) (outShorts string, outArgs []string, err error) { + if strings.HasPrefix(shorthands, "test.") { + return + } + outArgs = args + outShorts = shorthands[1:] + c := shorthands[0] + + flag, alreadythere := f.shorthands[c] + if !alreadythere { + if c == 'h' { // special case for nice help message. + f.usage() + err = ErrHelp + return + } + //TODO continue on error + err = f.failf("unknown shorthand flag: %q in -%s", c, shorthands) + return + } + var value string + if len(shorthands) > 2 && shorthands[1] == '=' { + value = shorthands[2:] + outShorts = "" + } else if len(flag.NoOptDefVal) > 0 { + value = flag.NoOptDefVal + } else if len(shorthands) > 1 { + value = shorthands[1:] + outShorts = "" + } else if len(args) > 0 { + value = args[0] + outArgs = args[1:] + } else { + err = f.failf("flag needs an argument: %q in -%s", c, shorthands) + return + } + err = fn(flag, value, shorthands) + return +} + +func (f *FlagSet) parseShortArg(s string, args []string, fn parseFunc) (a []string, err error) { + a = args + shorthands := s[1:] + + for len(shorthands) > 0 { + shorthands, a, err = f.parseSingleShortArg(shorthands, args, fn) + if err != nil { + return + } + } + + return +} + +func (f *FlagSet) parseArgs(args []string, fn parseFunc) (err error) { + for len(args) > 0 { + s := args[0] + args = args[1:] + if len(s) == 0 || s[0] != '-' || len(s) == 1 { + if !f.interspersed { + f.args = append(f.args, s) + f.args = append(f.args, args...) + return nil + } + f.args = append(f.args, s) + continue + } + + if s[1] == '-' { + if len(s) == 2 { // "--" terminates the flags + f.argsLenAtDash = len(f.args) + f.args = append(f.args, args...) + break + } + args, err = f.parseLongArg(s, args, fn) + } else { + args, err = f.parseShortArg(s, args, fn) + } + if err != nil { + return + } + } + return +} + +// Parse parses flag definitions from the argument list, which should not +// include the command name. Must be called after all flags in the FlagSet +// are defined and before flags are accessed by the program. +// The return value will be ErrHelp if -help was set but not defined. +func (f *FlagSet) Parse(arguments []string) error { + f.parsed = true + f.args = make([]string, 0, len(arguments)) + + assign := func(flag *Flag, value, origArg string) error { + return f.setFlag(flag, value, origArg) + } + + err := f.parseArgs(arguments, assign) + if err != nil { + switch f.errorHandling { + case ContinueOnError: + return err + case ExitOnError: + os.Exit(2) + case PanicOnError: + panic(err) + } + } + return nil +} + +type parseFunc func(flag *Flag, value, origArg string) error + +// ParseAll parses flag definitions from the argument list, which should not +// include the command name. The arguments for fn are flag and value. Must be +// called after all flags in the FlagSet are defined and before flags are +// accessed by the program. The return value will be ErrHelp if -help was set +// but not defined. +func (f *FlagSet) ParseAll(arguments []string, fn func(flag *Flag, value string) error) error { + f.parsed = true + f.args = make([]string, 0, len(arguments)) + + assign := func(flag *Flag, value, origArg string) error { + return fn(flag, value) + } + + err := f.parseArgs(arguments, assign) + if err != nil { + switch f.errorHandling { + case ContinueOnError: + return err + case ExitOnError: + os.Exit(2) + case PanicOnError: + panic(err) + } + } + return nil +} + +// Parsed reports whether f.Parse has been called. +func (f *FlagSet) Parsed() bool { + return f.parsed +} + +// Parse parses the command-line flags from os.Args[1:]. Must be called +// after all flags are defined and before flags are accessed by the program. +func Parse() { + // Ignore errors; CommandLine is set for ExitOnError. + CommandLine.Parse(os.Args[1:]) +} + +// ParseAll parses the command-line flags from os.Args[1:] and called fn for each. +// The arguments for fn are flag and value. Must be called after all flags are +// defined and before flags are accessed by the program. +func ParseAll(fn func(flag *Flag, value string) error) { + // Ignore errors; CommandLine is set for ExitOnError. + CommandLine.ParseAll(os.Args[1:], fn) +} + +// SetInterspersed sets whether to support interspersed option/non-option arguments. +func SetInterspersed(interspersed bool) { + CommandLine.SetInterspersed(interspersed) +} + +// Parsed returns true if the command-line flags have been parsed. +func Parsed() bool { + return CommandLine.Parsed() +} + +// CommandLine is the default set of command-line flags, parsed from os.Args. +var CommandLine = NewFlagSet(os.Args[0], ExitOnError) + +// NewFlagSet returns a new, empty flag set with the specified name and +// error handling property. +func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet { + f := &FlagSet{ + name: name, + errorHandling: errorHandling, + argsLenAtDash: -1, + interspersed: true, + } + return f +} + +// SetInterspersed sets whether to support interspersed option/non-option arguments. +func (f *FlagSet) SetInterspersed(interspersed bool) { + f.interspersed = interspersed +} + +// Init sets the name and error handling property for a flag set. +// By default, the zero FlagSet uses an empty name and the +// ContinueOnError error handling policy. +func (f *FlagSet) Init(name string, errorHandling ErrorHandling) { + f.name = name + f.errorHandling = errorHandling + f.argsLenAtDash = -1 +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/float32.go b/cmd/bleve/vendor/github.com/spf13/pflag/float32.go new file mode 100644 index 0000000..a243f81 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/float32.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- float32 Value +type float32Value float32 + +func newFloat32Value(val float32, p *float32) *float32Value { + *p = val + return (*float32Value)(p) +} + +func (f *float32Value) Set(s string) error { + v, err := strconv.ParseFloat(s, 32) + *f = float32Value(v) + return err +} + +func (f *float32Value) Type() string { + return "float32" +} + +func (f *float32Value) String() string { return strconv.FormatFloat(float64(*f), 'g', -1, 32) } + +func float32Conv(sval string) (interface{}, error) { + v, err := strconv.ParseFloat(sval, 32) + if err != nil { + return 0, err + } + return float32(v), nil +} + +// GetFloat32 return the float32 value of a flag with the given name +func (f *FlagSet) GetFloat32(name string) (float32, error) { + val, err := f.getFlagType(name, "float32", float32Conv) + if err != nil { + return 0, err + } + return val.(float32), nil +} + +// Float32Var defines a float32 flag with specified name, default value, and usage string. +// The argument p points to a float32 variable in which to store the value of the flag. +func (f *FlagSet) Float32Var(p *float32, name string, value float32, usage string) { + f.VarP(newFloat32Value(value, p), name, "", usage) +} + +// Float32VarP is like Float32Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float32VarP(p *float32, name, shorthand string, value float32, usage string) { + f.VarP(newFloat32Value(value, p), name, shorthand, usage) +} + +// Float32Var defines a float32 flag with specified name, default value, and usage string. +// The argument p points to a float32 variable in which to store the value of the flag. +func Float32Var(p *float32, name string, value float32, usage string) { + CommandLine.VarP(newFloat32Value(value, p), name, "", usage) +} + +// Float32VarP is like Float32Var, but accepts a shorthand letter that can be used after a single dash. +func Float32VarP(p *float32, name, shorthand string, value float32, usage string) { + CommandLine.VarP(newFloat32Value(value, p), name, shorthand, usage) +} + +// Float32 defines a float32 flag with specified name, default value, and usage string. +// The return value is the address of a float32 variable that stores the value of the flag. +func (f *FlagSet) Float32(name string, value float32, usage string) *float32 { + p := new(float32) + f.Float32VarP(p, name, "", value, usage) + return p +} + +// Float32P is like Float32, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float32P(name, shorthand string, value float32, usage string) *float32 { + p := new(float32) + f.Float32VarP(p, name, shorthand, value, usage) + return p +} + +// Float32 defines a float32 flag with specified name, default value, and usage string. +// The return value is the address of a float32 variable that stores the value of the flag. +func Float32(name string, value float32, usage string) *float32 { + return CommandLine.Float32P(name, "", value, usage) +} + +// Float32P is like Float32, but accepts a shorthand letter that can be used after a single dash. +func Float32P(name, shorthand string, value float32, usage string) *float32 { + return CommandLine.Float32P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/float64.go b/cmd/bleve/vendor/github.com/spf13/pflag/float64.go new file mode 100644 index 0000000..04b5492 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/float64.go @@ -0,0 +1,84 @@ +package pflag + +import "strconv" + +// -- float64 Value +type float64Value float64 + +func newFloat64Value(val float64, p *float64) *float64Value { + *p = val + return (*float64Value)(p) +} + +func (f *float64Value) Set(s string) error { + v, err := strconv.ParseFloat(s, 64) + *f = float64Value(v) + return err +} + +func (f *float64Value) Type() string { + return "float64" +} + +func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f), 'g', -1, 64) } + +func float64Conv(sval string) (interface{}, error) { + return strconv.ParseFloat(sval, 64) +} + +// GetFloat64 return the float64 value of a flag with the given name +func (f *FlagSet) GetFloat64(name string) (float64, error) { + val, err := f.getFlagType(name, "float64", float64Conv) + if err != nil { + return 0, err + } + return val.(float64), nil +} + +// Float64Var defines a float64 flag with specified name, default value, and usage string. +// The argument p points to a float64 variable in which to store the value of the flag. +func (f *FlagSet) Float64Var(p *float64, name string, value float64, usage string) { + f.VarP(newFloat64Value(value, p), name, "", usage) +} + +// Float64VarP is like Float64Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float64VarP(p *float64, name, shorthand string, value float64, usage string) { + f.VarP(newFloat64Value(value, p), name, shorthand, usage) +} + +// Float64Var defines a float64 flag with specified name, default value, and usage string. +// The argument p points to a float64 variable in which to store the value of the flag. +func Float64Var(p *float64, name string, value float64, usage string) { + CommandLine.VarP(newFloat64Value(value, p), name, "", usage) +} + +// Float64VarP is like Float64Var, but accepts a shorthand letter that can be used after a single dash. +func Float64VarP(p *float64, name, shorthand string, value float64, usage string) { + CommandLine.VarP(newFloat64Value(value, p), name, shorthand, usage) +} + +// Float64 defines a float64 flag with specified name, default value, and usage string. +// The return value is the address of a float64 variable that stores the value of the flag. +func (f *FlagSet) Float64(name string, value float64, usage string) *float64 { + p := new(float64) + f.Float64VarP(p, name, "", value, usage) + return p +} + +// Float64P is like Float64, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float64P(name, shorthand string, value float64, usage string) *float64 { + p := new(float64) + f.Float64VarP(p, name, shorthand, value, usage) + return p +} + +// Float64 defines a float64 flag with specified name, default value, and usage string. +// The return value is the address of a float64 variable that stores the value of the flag. +func Float64(name string, value float64, usage string) *float64 { + return CommandLine.Float64P(name, "", value, usage) +} + +// Float64P is like Float64, but accepts a shorthand letter that can be used after a single dash. +func Float64P(name, shorthand string, value float64, usage string) *float64 { + return CommandLine.Float64P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/golangflag.go b/cmd/bleve/vendor/github.com/spf13/pflag/golangflag.go new file mode 100644 index 0000000..c4f47eb --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/golangflag.go @@ -0,0 +1,101 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pflag + +import ( + goflag "flag" + "reflect" + "strings" +) + +// flagValueWrapper implements pflag.Value around a flag.Value. The main +// difference here is the addition of the Type method that returns a string +// name of the type. As this is generally unknown, we approximate that with +// reflection. +type flagValueWrapper struct { + inner goflag.Value + flagType string +} + +// We are just copying the boolFlag interface out of goflag as that is what +// they use to decide if a flag should get "true" when no arg is given. +type goBoolFlag interface { + goflag.Value + IsBoolFlag() bool +} + +func wrapFlagValue(v goflag.Value) Value { + // If the flag.Value happens to also be a pflag.Value, just use it directly. + if pv, ok := v.(Value); ok { + return pv + } + + pv := &flagValueWrapper{ + inner: v, + } + + t := reflect.TypeOf(v) + if t.Kind() == reflect.Interface || t.Kind() == reflect.Ptr { + t = t.Elem() + } + + pv.flagType = strings.TrimSuffix(t.Name(), "Value") + return pv +} + +func (v *flagValueWrapper) String() string { + return v.inner.String() +} + +func (v *flagValueWrapper) Set(s string) error { + return v.inner.Set(s) +} + +func (v *flagValueWrapper) Type() string { + return v.flagType +} + +// PFlagFromGoFlag will return a *pflag.Flag given a *flag.Flag +// If the *flag.Flag.Name was a single character (ex: `v`) it will be accessiblei +// with both `-v` and `--v` in flags. If the golang flag was more than a single +// character (ex: `verbose`) it will only be accessible via `--verbose` +func PFlagFromGoFlag(goflag *goflag.Flag) *Flag { + // Remember the default value as a string; it won't change. + flag := &Flag{ + Name: goflag.Name, + Usage: goflag.Usage, + Value: wrapFlagValue(goflag.Value), + // Looks like golang flags don't set DefValue correctly :-( + //DefValue: goflag.DefValue, + DefValue: goflag.Value.String(), + } + // Ex: if the golang flag was -v, allow both -v and --v to work + if len(flag.Name) == 1 { + flag.Shorthand = flag.Name + } + if fv, ok := goflag.Value.(goBoolFlag); ok && fv.IsBoolFlag() { + flag.NoOptDefVal = "true" + } + return flag +} + +// AddGoFlag will add the given *flag.Flag to the pflag.FlagSet +func (f *FlagSet) AddGoFlag(goflag *goflag.Flag) { + if f.Lookup(goflag.Name) != nil { + return + } + newflag := PFlagFromGoFlag(goflag) + f.AddFlag(newflag) +} + +// AddGoFlagSet will add the given *flag.FlagSet to the pflag.FlagSet +func (f *FlagSet) AddGoFlagSet(newSet *goflag.FlagSet) { + if newSet == nil { + return + } + newSet.VisitAll(func(goflag *goflag.Flag) { + f.AddGoFlag(goflag) + }) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/int.go b/cmd/bleve/vendor/github.com/spf13/pflag/int.go new file mode 100644 index 0000000..1474b89 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/int.go @@ -0,0 +1,84 @@ +package pflag + +import "strconv" + +// -- int Value +type intValue int + +func newIntValue(val int, p *int) *intValue { + *p = val + return (*intValue)(p) +} + +func (i *intValue) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + *i = intValue(v) + return err +} + +func (i *intValue) Type() string { + return "int" +} + +func (i *intValue) String() string { return strconv.Itoa(int(*i)) } + +func intConv(sval string) (interface{}, error) { + return strconv.Atoi(sval) +} + +// GetInt return the int value of a flag with the given name +func (f *FlagSet) GetInt(name string) (int, error) { + val, err := f.getFlagType(name, "int", intConv) + if err != nil { + return 0, err + } + return val.(int), nil +} + +// IntVar defines an int flag with specified name, default value, and usage string. +// The argument p points to an int variable in which to store the value of the flag. +func (f *FlagSet) IntVar(p *int, name string, value int, usage string) { + f.VarP(newIntValue(value, p), name, "", usage) +} + +// IntVarP is like IntVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IntVarP(p *int, name, shorthand string, value int, usage string) { + f.VarP(newIntValue(value, p), name, shorthand, usage) +} + +// IntVar defines an int flag with specified name, default value, and usage string. +// The argument p points to an int variable in which to store the value of the flag. +func IntVar(p *int, name string, value int, usage string) { + CommandLine.VarP(newIntValue(value, p), name, "", usage) +} + +// IntVarP is like IntVar, but accepts a shorthand letter that can be used after a single dash. +func IntVarP(p *int, name, shorthand string, value int, usage string) { + CommandLine.VarP(newIntValue(value, p), name, shorthand, usage) +} + +// Int defines an int flag with specified name, default value, and usage string. +// The return value is the address of an int variable that stores the value of the flag. +func (f *FlagSet) Int(name string, value int, usage string) *int { + p := new(int) + f.IntVarP(p, name, "", value, usage) + return p +} + +// IntP is like Int, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IntP(name, shorthand string, value int, usage string) *int { + p := new(int) + f.IntVarP(p, name, shorthand, value, usage) + return p +} + +// Int defines an int flag with specified name, default value, and usage string. +// The return value is the address of an int variable that stores the value of the flag. +func Int(name string, value int, usage string) *int { + return CommandLine.IntP(name, "", value, usage) +} + +// IntP is like Int, but accepts a shorthand letter that can be used after a single dash. +func IntP(name, shorthand string, value int, usage string) *int { + return CommandLine.IntP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/int32.go b/cmd/bleve/vendor/github.com/spf13/pflag/int32.go new file mode 100644 index 0000000..9b95944 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/int32.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- int32 Value +type int32Value int32 + +func newInt32Value(val int32, p *int32) *int32Value { + *p = val + return (*int32Value)(p) +} + +func (i *int32Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 32) + *i = int32Value(v) + return err +} + +func (i *int32Value) Type() string { + return "int32" +} + +func (i *int32Value) String() string { return strconv.FormatInt(int64(*i), 10) } + +func int32Conv(sval string) (interface{}, error) { + v, err := strconv.ParseInt(sval, 0, 32) + if err != nil { + return 0, err + } + return int32(v), nil +} + +// GetInt32 return the int32 value of a flag with the given name +func (f *FlagSet) GetInt32(name string) (int32, error) { + val, err := f.getFlagType(name, "int32", int32Conv) + if err != nil { + return 0, err + } + return val.(int32), nil +} + +// Int32Var defines an int32 flag with specified name, default value, and usage string. +// The argument p points to an int32 variable in which to store the value of the flag. +func (f *FlagSet) Int32Var(p *int32, name string, value int32, usage string) { + f.VarP(newInt32Value(value, p), name, "", usage) +} + +// Int32VarP is like Int32Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int32VarP(p *int32, name, shorthand string, value int32, usage string) { + f.VarP(newInt32Value(value, p), name, shorthand, usage) +} + +// Int32Var defines an int32 flag with specified name, default value, and usage string. +// The argument p points to an int32 variable in which to store the value of the flag. +func Int32Var(p *int32, name string, value int32, usage string) { + CommandLine.VarP(newInt32Value(value, p), name, "", usage) +} + +// Int32VarP is like Int32Var, but accepts a shorthand letter that can be used after a single dash. +func Int32VarP(p *int32, name, shorthand string, value int32, usage string) { + CommandLine.VarP(newInt32Value(value, p), name, shorthand, usage) +} + +// Int32 defines an int32 flag with specified name, default value, and usage string. +// The return value is the address of an int32 variable that stores the value of the flag. +func (f *FlagSet) Int32(name string, value int32, usage string) *int32 { + p := new(int32) + f.Int32VarP(p, name, "", value, usage) + return p +} + +// Int32P is like Int32, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int32P(name, shorthand string, value int32, usage string) *int32 { + p := new(int32) + f.Int32VarP(p, name, shorthand, value, usage) + return p +} + +// Int32 defines an int32 flag with specified name, default value, and usage string. +// The return value is the address of an int32 variable that stores the value of the flag. +func Int32(name string, value int32, usage string) *int32 { + return CommandLine.Int32P(name, "", value, usage) +} + +// Int32P is like Int32, but accepts a shorthand letter that can be used after a single dash. +func Int32P(name, shorthand string, value int32, usage string) *int32 { + return CommandLine.Int32P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/int64.go b/cmd/bleve/vendor/github.com/spf13/pflag/int64.go new file mode 100644 index 0000000..0026d78 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/int64.go @@ -0,0 +1,84 @@ +package pflag + +import "strconv" + +// -- int64 Value +type int64Value int64 + +func newInt64Value(val int64, p *int64) *int64Value { + *p = val + return (*int64Value)(p) +} + +func (i *int64Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + *i = int64Value(v) + return err +} + +func (i *int64Value) Type() string { + return "int64" +} + +func (i *int64Value) String() string { return strconv.FormatInt(int64(*i), 10) } + +func int64Conv(sval string) (interface{}, error) { + return strconv.ParseInt(sval, 0, 64) +} + +// GetInt64 return the int64 value of a flag with the given name +func (f *FlagSet) GetInt64(name string) (int64, error) { + val, err := f.getFlagType(name, "int64", int64Conv) + if err != nil { + return 0, err + } + return val.(int64), nil +} + +// Int64Var defines an int64 flag with specified name, default value, and usage string. +// The argument p points to an int64 variable in which to store the value of the flag. +func (f *FlagSet) Int64Var(p *int64, name string, value int64, usage string) { + f.VarP(newInt64Value(value, p), name, "", usage) +} + +// Int64VarP is like Int64Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int64VarP(p *int64, name, shorthand string, value int64, usage string) { + f.VarP(newInt64Value(value, p), name, shorthand, usage) +} + +// Int64Var defines an int64 flag with specified name, default value, and usage string. +// The argument p points to an int64 variable in which to store the value of the flag. +func Int64Var(p *int64, name string, value int64, usage string) { + CommandLine.VarP(newInt64Value(value, p), name, "", usage) +} + +// Int64VarP is like Int64Var, but accepts a shorthand letter that can be used after a single dash. +func Int64VarP(p *int64, name, shorthand string, value int64, usage string) { + CommandLine.VarP(newInt64Value(value, p), name, shorthand, usage) +} + +// Int64 defines an int64 flag with specified name, default value, and usage string. +// The return value is the address of an int64 variable that stores the value of the flag. +func (f *FlagSet) Int64(name string, value int64, usage string) *int64 { + p := new(int64) + f.Int64VarP(p, name, "", value, usage) + return p +} + +// Int64P is like Int64, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int64P(name, shorthand string, value int64, usage string) *int64 { + p := new(int64) + f.Int64VarP(p, name, shorthand, value, usage) + return p +} + +// Int64 defines an int64 flag with specified name, default value, and usage string. +// The return value is the address of an int64 variable that stores the value of the flag. +func Int64(name string, value int64, usage string) *int64 { + return CommandLine.Int64P(name, "", value, usage) +} + +// Int64P is like Int64, but accepts a shorthand letter that can be used after a single dash. +func Int64P(name, shorthand string, value int64, usage string) *int64 { + return CommandLine.Int64P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/int8.go b/cmd/bleve/vendor/github.com/spf13/pflag/int8.go new file mode 100644 index 0000000..4da9222 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/int8.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- int8 Value +type int8Value int8 + +func newInt8Value(val int8, p *int8) *int8Value { + *p = val + return (*int8Value)(p) +} + +func (i *int8Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 8) + *i = int8Value(v) + return err +} + +func (i *int8Value) Type() string { + return "int8" +} + +func (i *int8Value) String() string { return strconv.FormatInt(int64(*i), 10) } + +func int8Conv(sval string) (interface{}, error) { + v, err := strconv.ParseInt(sval, 0, 8) + if err != nil { + return 0, err + } + return int8(v), nil +} + +// GetInt8 return the int8 value of a flag with the given name +func (f *FlagSet) GetInt8(name string) (int8, error) { + val, err := f.getFlagType(name, "int8", int8Conv) + if err != nil { + return 0, err + } + return val.(int8), nil +} + +// Int8Var defines an int8 flag with specified name, default value, and usage string. +// The argument p points to an int8 variable in which to store the value of the flag. +func (f *FlagSet) Int8Var(p *int8, name string, value int8, usage string) { + f.VarP(newInt8Value(value, p), name, "", usage) +} + +// Int8VarP is like Int8Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int8VarP(p *int8, name, shorthand string, value int8, usage string) { + f.VarP(newInt8Value(value, p), name, shorthand, usage) +} + +// Int8Var defines an int8 flag with specified name, default value, and usage string. +// The argument p points to an int8 variable in which to store the value of the flag. +func Int8Var(p *int8, name string, value int8, usage string) { + CommandLine.VarP(newInt8Value(value, p), name, "", usage) +} + +// Int8VarP is like Int8Var, but accepts a shorthand letter that can be used after a single dash. +func Int8VarP(p *int8, name, shorthand string, value int8, usage string) { + CommandLine.VarP(newInt8Value(value, p), name, shorthand, usage) +} + +// Int8 defines an int8 flag with specified name, default value, and usage string. +// The return value is the address of an int8 variable that stores the value of the flag. +func (f *FlagSet) Int8(name string, value int8, usage string) *int8 { + p := new(int8) + f.Int8VarP(p, name, "", value, usage) + return p +} + +// Int8P is like Int8, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int8P(name, shorthand string, value int8, usage string) *int8 { + p := new(int8) + f.Int8VarP(p, name, shorthand, value, usage) + return p +} + +// Int8 defines an int8 flag with specified name, default value, and usage string. +// The return value is the address of an int8 variable that stores the value of the flag. +func Int8(name string, value int8, usage string) *int8 { + return CommandLine.Int8P(name, "", value, usage) +} + +// Int8P is like Int8, but accepts a shorthand letter that can be used after a single dash. +func Int8P(name, shorthand string, value int8, usage string) *int8 { + return CommandLine.Int8P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/int_slice.go b/cmd/bleve/vendor/github.com/spf13/pflag/int_slice.go new file mode 100644 index 0000000..1e7c9ed --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/int_slice.go @@ -0,0 +1,128 @@ +package pflag + +import ( + "fmt" + "strconv" + "strings" +) + +// -- intSlice Value +type intSliceValue struct { + value *[]int + changed bool +} + +func newIntSliceValue(val []int, p *[]int) *intSliceValue { + isv := new(intSliceValue) + isv.value = p + *isv.value = val + return isv +} + +func (s *intSliceValue) Set(val string) error { + ss := strings.Split(val, ",") + out := make([]int, len(ss)) + for i, d := range ss { + var err error + out[i], err = strconv.Atoi(d) + if err != nil { + return err + } + + } + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + s.changed = true + return nil +} + +func (s *intSliceValue) Type() string { + return "intSlice" +} + +func (s *intSliceValue) String() string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = fmt.Sprintf("%d", d) + } + return "[" + strings.Join(out, ",") + "]" +} + +func intSliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Empty string would cause a slice with one (empty) entry + if len(val) == 0 { + return []int{}, nil + } + ss := strings.Split(val, ",") + out := make([]int, len(ss)) + for i, d := range ss { + var err error + out[i], err = strconv.Atoi(d) + if err != nil { + return nil, err + } + + } + return out, nil +} + +// GetIntSlice return the []int value of a flag with the given name +func (f *FlagSet) GetIntSlice(name string) ([]int, error) { + val, err := f.getFlagType(name, "intSlice", intSliceConv) + if err != nil { + return []int{}, err + } + return val.([]int), nil +} + +// IntSliceVar defines a intSlice flag with specified name, default value, and usage string. +// The argument p points to a []int variable in which to store the value of the flag. +func (f *FlagSet) IntSliceVar(p *[]int, name string, value []int, usage string) { + f.VarP(newIntSliceValue(value, p), name, "", usage) +} + +// IntSliceVarP is like IntSliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IntSliceVarP(p *[]int, name, shorthand string, value []int, usage string) { + f.VarP(newIntSliceValue(value, p), name, shorthand, usage) +} + +// IntSliceVar defines a int[] flag with specified name, default value, and usage string. +// The argument p points to a int[] variable in which to store the value of the flag. +func IntSliceVar(p *[]int, name string, value []int, usage string) { + CommandLine.VarP(newIntSliceValue(value, p), name, "", usage) +} + +// IntSliceVarP is like IntSliceVar, but accepts a shorthand letter that can be used after a single dash. +func IntSliceVarP(p *[]int, name, shorthand string, value []int, usage string) { + CommandLine.VarP(newIntSliceValue(value, p), name, shorthand, usage) +} + +// IntSlice defines a []int flag with specified name, default value, and usage string. +// The return value is the address of a []int variable that stores the value of the flag. +func (f *FlagSet) IntSlice(name string, value []int, usage string) *[]int { + p := []int{} + f.IntSliceVarP(&p, name, "", value, usage) + return &p +} + +// IntSliceP is like IntSlice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IntSliceP(name, shorthand string, value []int, usage string) *[]int { + p := []int{} + f.IntSliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// IntSlice defines a []int flag with specified name, default value, and usage string. +// The return value is the address of a []int variable that stores the value of the flag. +func IntSlice(name string, value []int, usage string) *[]int { + return CommandLine.IntSliceP(name, "", value, usage) +} + +// IntSliceP is like IntSlice, but accepts a shorthand letter that can be used after a single dash. +func IntSliceP(name, shorthand string, value []int, usage string) *[]int { + return CommandLine.IntSliceP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/ip.go b/cmd/bleve/vendor/github.com/spf13/pflag/ip.go new file mode 100644 index 0000000..3d414ba --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/ip.go @@ -0,0 +1,94 @@ +package pflag + +import ( + "fmt" + "net" + "strings" +) + +// -- net.IP value +type ipValue net.IP + +func newIPValue(val net.IP, p *net.IP) *ipValue { + *p = val + return (*ipValue)(p) +} + +func (i *ipValue) String() string { return net.IP(*i).String() } +func (i *ipValue) Set(s string) error { + ip := net.ParseIP(strings.TrimSpace(s)) + if ip == nil { + return fmt.Errorf("failed to parse IP: %q", s) + } + *i = ipValue(ip) + return nil +} + +func (i *ipValue) Type() string { + return "ip" +} + +func ipConv(sval string) (interface{}, error) { + ip := net.ParseIP(sval) + if ip != nil { + return ip, nil + } + return nil, fmt.Errorf("invalid string being converted to IP address: %s", sval) +} + +// GetIP return the net.IP value of a flag with the given name +func (f *FlagSet) GetIP(name string) (net.IP, error) { + val, err := f.getFlagType(name, "ip", ipConv) + if err != nil { + return nil, err + } + return val.(net.IP), nil +} + +// IPVar defines an net.IP flag with specified name, default value, and usage string. +// The argument p points to an net.IP variable in which to store the value of the flag. +func (f *FlagSet) IPVar(p *net.IP, name string, value net.IP, usage string) { + f.VarP(newIPValue(value, p), name, "", usage) +} + +// IPVarP is like IPVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPVarP(p *net.IP, name, shorthand string, value net.IP, usage string) { + f.VarP(newIPValue(value, p), name, shorthand, usage) +} + +// IPVar defines an net.IP flag with specified name, default value, and usage string. +// The argument p points to an net.IP variable in which to store the value of the flag. +func IPVar(p *net.IP, name string, value net.IP, usage string) { + CommandLine.VarP(newIPValue(value, p), name, "", usage) +} + +// IPVarP is like IPVar, but accepts a shorthand letter that can be used after a single dash. +func IPVarP(p *net.IP, name, shorthand string, value net.IP, usage string) { + CommandLine.VarP(newIPValue(value, p), name, shorthand, usage) +} + +// IP defines an net.IP flag with specified name, default value, and usage string. +// The return value is the address of an net.IP variable that stores the value of the flag. +func (f *FlagSet) IP(name string, value net.IP, usage string) *net.IP { + p := new(net.IP) + f.IPVarP(p, name, "", value, usage) + return p +} + +// IPP is like IP, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPP(name, shorthand string, value net.IP, usage string) *net.IP { + p := new(net.IP) + f.IPVarP(p, name, shorthand, value, usage) + return p +} + +// IP defines an net.IP flag with specified name, default value, and usage string. +// The return value is the address of an net.IP variable that stores the value of the flag. +func IP(name string, value net.IP, usage string) *net.IP { + return CommandLine.IPP(name, "", value, usage) +} + +// IPP is like IP, but accepts a shorthand letter that can be used after a single dash. +func IPP(name, shorthand string, value net.IP, usage string) *net.IP { + return CommandLine.IPP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/ip_slice.go b/cmd/bleve/vendor/github.com/spf13/pflag/ip_slice.go new file mode 100644 index 0000000..7dd196f --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/ip_slice.go @@ -0,0 +1,148 @@ +package pflag + +import ( + "fmt" + "io" + "net" + "strings" +) + +// -- ipSlice Value +type ipSliceValue struct { + value *[]net.IP + changed bool +} + +func newIPSliceValue(val []net.IP, p *[]net.IP) *ipSliceValue { + ipsv := new(ipSliceValue) + ipsv.value = p + *ipsv.value = val + return ipsv +} + +// Set converts, and assigns, the comma-separated IP argument string representation as the []net.IP value of this flag. +// If Set is called on a flag that already has a []net.IP assigned, the newly converted values will be appended. +func (s *ipSliceValue) Set(val string) error { + + // remove all quote characters + rmQuote := strings.NewReplacer(`"`, "", `'`, "", "`", "") + + // read flag arguments with CSV parser + ipStrSlice, err := readAsCSV(rmQuote.Replace(val)) + if err != nil && err != io.EOF { + return err + } + + // parse ip values into slice + out := make([]net.IP, 0, len(ipStrSlice)) + for _, ipStr := range ipStrSlice { + ip := net.ParseIP(strings.TrimSpace(ipStr)) + if ip == nil { + return fmt.Errorf("invalid string being converted to IP address: %s", ipStr) + } + out = append(out, ip) + } + + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + + s.changed = true + + return nil +} + +// Type returns a string that uniquely represents this flag's type. +func (s *ipSliceValue) Type() string { + return "ipSlice" +} + +// String defines a "native" format for this net.IP slice flag value. +func (s *ipSliceValue) String() string { + + ipStrSlice := make([]string, len(*s.value)) + for i, ip := range *s.value { + ipStrSlice[i] = ip.String() + } + + out, _ := writeAsCSV(ipStrSlice) + + return "[" + out + "]" +} + +func ipSliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Emtpy string would cause a slice with one (empty) entry + if len(val) == 0 { + return []net.IP{}, nil + } + ss := strings.Split(val, ",") + out := make([]net.IP, len(ss)) + for i, sval := range ss { + ip := net.ParseIP(strings.TrimSpace(sval)) + if ip == nil { + return nil, fmt.Errorf("invalid string being converted to IP address: %s", sval) + } + out[i] = ip + } + return out, nil +} + +// GetIPSlice returns the []net.IP value of a flag with the given name +func (f *FlagSet) GetIPSlice(name string) ([]net.IP, error) { + val, err := f.getFlagType(name, "ipSlice", ipSliceConv) + if err != nil { + return []net.IP{}, err + } + return val.([]net.IP), nil +} + +// IPSliceVar defines a ipSlice flag with specified name, default value, and usage string. +// The argument p points to a []net.IP variable in which to store the value of the flag. +func (f *FlagSet) IPSliceVar(p *[]net.IP, name string, value []net.IP, usage string) { + f.VarP(newIPSliceValue(value, p), name, "", usage) +} + +// IPSliceVarP is like IPSliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPSliceVarP(p *[]net.IP, name, shorthand string, value []net.IP, usage string) { + f.VarP(newIPSliceValue(value, p), name, shorthand, usage) +} + +// IPSliceVar defines a []net.IP flag with specified name, default value, and usage string. +// The argument p points to a []net.IP variable in which to store the value of the flag. +func IPSliceVar(p *[]net.IP, name string, value []net.IP, usage string) { + CommandLine.VarP(newIPSliceValue(value, p), name, "", usage) +} + +// IPSliceVarP is like IPSliceVar, but accepts a shorthand letter that can be used after a single dash. +func IPSliceVarP(p *[]net.IP, name, shorthand string, value []net.IP, usage string) { + CommandLine.VarP(newIPSliceValue(value, p), name, shorthand, usage) +} + +// IPSlice defines a []net.IP flag with specified name, default value, and usage string. +// The return value is the address of a []net.IP variable that stores the value of that flag. +func (f *FlagSet) IPSlice(name string, value []net.IP, usage string) *[]net.IP { + p := []net.IP{} + f.IPSliceVarP(&p, name, "", value, usage) + return &p +} + +// IPSliceP is like IPSlice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPSliceP(name, shorthand string, value []net.IP, usage string) *[]net.IP { + p := []net.IP{} + f.IPSliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// IPSlice defines a []net.IP flag with specified name, default value, and usage string. +// The return value is the address of a []net.IP variable that stores the value of the flag. +func IPSlice(name string, value []net.IP, usage string) *[]net.IP { + return CommandLine.IPSliceP(name, "", value, usage) +} + +// IPSliceP is like IPSlice, but accepts a shorthand letter that can be used after a single dash. +func IPSliceP(name, shorthand string, value []net.IP, usage string) *[]net.IP { + return CommandLine.IPSliceP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/ipmask.go b/cmd/bleve/vendor/github.com/spf13/pflag/ipmask.go new file mode 100644 index 0000000..5bd44bd --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/ipmask.go @@ -0,0 +1,122 @@ +package pflag + +import ( + "fmt" + "net" + "strconv" +) + +// -- net.IPMask value +type ipMaskValue net.IPMask + +func newIPMaskValue(val net.IPMask, p *net.IPMask) *ipMaskValue { + *p = val + return (*ipMaskValue)(p) +} + +func (i *ipMaskValue) String() string { return net.IPMask(*i).String() } +func (i *ipMaskValue) Set(s string) error { + ip := ParseIPv4Mask(s) + if ip == nil { + return fmt.Errorf("failed to parse IP mask: %q", s) + } + *i = ipMaskValue(ip) + return nil +} + +func (i *ipMaskValue) Type() string { + return "ipMask" +} + +// ParseIPv4Mask written in IP form (e.g. 255.255.255.0). +// This function should really belong to the net package. +func ParseIPv4Mask(s string) net.IPMask { + mask := net.ParseIP(s) + if mask == nil { + if len(s) != 8 { + return nil + } + // net.IPMask.String() actually outputs things like ffffff00 + // so write a horrible parser for that as well :-( + m := []int{} + for i := 0; i < 4; i++ { + b := "0x" + s[2*i:2*i+2] + d, err := strconv.ParseInt(b, 0, 0) + if err != nil { + return nil + } + m = append(m, int(d)) + } + s := fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3]) + mask = net.ParseIP(s) + if mask == nil { + return nil + } + } + return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) +} + +func parseIPv4Mask(sval string) (interface{}, error) { + mask := ParseIPv4Mask(sval) + if mask == nil { + return nil, fmt.Errorf("unable to parse %s as net.IPMask", sval) + } + return mask, nil +} + +// GetIPv4Mask return the net.IPv4Mask value of a flag with the given name +func (f *FlagSet) GetIPv4Mask(name string) (net.IPMask, error) { + val, err := f.getFlagType(name, "ipMask", parseIPv4Mask) + if err != nil { + return nil, err + } + return val.(net.IPMask), nil +} + +// IPMaskVar defines an net.IPMask flag with specified name, default value, and usage string. +// The argument p points to an net.IPMask variable in which to store the value of the flag. +func (f *FlagSet) IPMaskVar(p *net.IPMask, name string, value net.IPMask, usage string) { + f.VarP(newIPMaskValue(value, p), name, "", usage) +} + +// IPMaskVarP is like IPMaskVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPMaskVarP(p *net.IPMask, name, shorthand string, value net.IPMask, usage string) { + f.VarP(newIPMaskValue(value, p), name, shorthand, usage) +} + +// IPMaskVar defines an net.IPMask flag with specified name, default value, and usage string. +// The argument p points to an net.IPMask variable in which to store the value of the flag. +func IPMaskVar(p *net.IPMask, name string, value net.IPMask, usage string) { + CommandLine.VarP(newIPMaskValue(value, p), name, "", usage) +} + +// IPMaskVarP is like IPMaskVar, but accepts a shorthand letter that can be used after a single dash. +func IPMaskVarP(p *net.IPMask, name, shorthand string, value net.IPMask, usage string) { + CommandLine.VarP(newIPMaskValue(value, p), name, shorthand, usage) +} + +// IPMask defines an net.IPMask flag with specified name, default value, and usage string. +// The return value is the address of an net.IPMask variable that stores the value of the flag. +func (f *FlagSet) IPMask(name string, value net.IPMask, usage string) *net.IPMask { + p := new(net.IPMask) + f.IPMaskVarP(p, name, "", value, usage) + return p +} + +// IPMaskP is like IPMask, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPMaskP(name, shorthand string, value net.IPMask, usage string) *net.IPMask { + p := new(net.IPMask) + f.IPMaskVarP(p, name, shorthand, value, usage) + return p +} + +// IPMask defines an net.IPMask flag with specified name, default value, and usage string. +// The return value is the address of an net.IPMask variable that stores the value of the flag. +func IPMask(name string, value net.IPMask, usage string) *net.IPMask { + return CommandLine.IPMaskP(name, "", value, usage) +} + +// IPMaskP is like IP, but accepts a shorthand letter that can be used after a single dash. +func IPMaskP(name, shorthand string, value net.IPMask, usage string) *net.IPMask { + return CommandLine.IPMaskP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/ipnet.go b/cmd/bleve/vendor/github.com/spf13/pflag/ipnet.go new file mode 100644 index 0000000..e2c1b8b --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/ipnet.go @@ -0,0 +1,98 @@ +package pflag + +import ( + "fmt" + "net" + "strings" +) + +// IPNet adapts net.IPNet for use as a flag. +type ipNetValue net.IPNet + +func (ipnet ipNetValue) String() string { + n := net.IPNet(ipnet) + return n.String() +} + +func (ipnet *ipNetValue) Set(value string) error { + _, n, err := net.ParseCIDR(strings.TrimSpace(value)) + if err != nil { + return err + } + *ipnet = ipNetValue(*n) + return nil +} + +func (*ipNetValue) Type() string { + return "ipNet" +} + +func newIPNetValue(val net.IPNet, p *net.IPNet) *ipNetValue { + *p = val + return (*ipNetValue)(p) +} + +func ipNetConv(sval string) (interface{}, error) { + _, n, err := net.ParseCIDR(strings.TrimSpace(sval)) + if err == nil { + return *n, nil + } + return nil, fmt.Errorf("invalid string being converted to IPNet: %s", sval) +} + +// GetIPNet return the net.IPNet value of a flag with the given name +func (f *FlagSet) GetIPNet(name string) (net.IPNet, error) { + val, err := f.getFlagType(name, "ipNet", ipNetConv) + if err != nil { + return net.IPNet{}, err + } + return val.(net.IPNet), nil +} + +// IPNetVar defines an net.IPNet flag with specified name, default value, and usage string. +// The argument p points to an net.IPNet variable in which to store the value of the flag. +func (f *FlagSet) IPNetVar(p *net.IPNet, name string, value net.IPNet, usage string) { + f.VarP(newIPNetValue(value, p), name, "", usage) +} + +// IPNetVarP is like IPNetVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPNetVarP(p *net.IPNet, name, shorthand string, value net.IPNet, usage string) { + f.VarP(newIPNetValue(value, p), name, shorthand, usage) +} + +// IPNetVar defines an net.IPNet flag with specified name, default value, and usage string. +// The argument p points to an net.IPNet variable in which to store the value of the flag. +func IPNetVar(p *net.IPNet, name string, value net.IPNet, usage string) { + CommandLine.VarP(newIPNetValue(value, p), name, "", usage) +} + +// IPNetVarP is like IPNetVar, but accepts a shorthand letter that can be used after a single dash. +func IPNetVarP(p *net.IPNet, name, shorthand string, value net.IPNet, usage string) { + CommandLine.VarP(newIPNetValue(value, p), name, shorthand, usage) +} + +// IPNet defines an net.IPNet flag with specified name, default value, and usage string. +// The return value is the address of an net.IPNet variable that stores the value of the flag. +func (f *FlagSet) IPNet(name string, value net.IPNet, usage string) *net.IPNet { + p := new(net.IPNet) + f.IPNetVarP(p, name, "", value, usage) + return p +} + +// IPNetP is like IPNet, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) IPNetP(name, shorthand string, value net.IPNet, usage string) *net.IPNet { + p := new(net.IPNet) + f.IPNetVarP(p, name, shorthand, value, usage) + return p +} + +// IPNet defines an net.IPNet flag with specified name, default value, and usage string. +// The return value is the address of an net.IPNet variable that stores the value of the flag. +func IPNet(name string, value net.IPNet, usage string) *net.IPNet { + return CommandLine.IPNetP(name, "", value, usage) +} + +// IPNetP is like IPNet, but accepts a shorthand letter that can be used after a single dash. +func IPNetP(name, shorthand string, value net.IPNet, usage string) *net.IPNet { + return CommandLine.IPNetP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/string.go b/cmd/bleve/vendor/github.com/spf13/pflag/string.go new file mode 100644 index 0000000..04e0a26 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/string.go @@ -0,0 +1,80 @@ +package pflag + +// -- string Value +type stringValue string + +func newStringValue(val string, p *string) *stringValue { + *p = val + return (*stringValue)(p) +} + +func (s *stringValue) Set(val string) error { + *s = stringValue(val) + return nil +} +func (s *stringValue) Type() string { + return "string" +} + +func (s *stringValue) String() string { return string(*s) } + +func stringConv(sval string) (interface{}, error) { + return sval, nil +} + +// GetString return the string value of a flag with the given name +func (f *FlagSet) GetString(name string) (string, error) { + val, err := f.getFlagType(name, "string", stringConv) + if err != nil { + return "", err + } + return val.(string), nil +} + +// StringVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a string variable in which to store the value of the flag. +func (f *FlagSet) StringVar(p *string, name string, value string, usage string) { + f.VarP(newStringValue(value, p), name, "", usage) +} + +// StringVarP is like StringVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringVarP(p *string, name, shorthand string, value string, usage string) { + f.VarP(newStringValue(value, p), name, shorthand, usage) +} + +// StringVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a string variable in which to store the value of the flag. +func StringVar(p *string, name string, value string, usage string) { + CommandLine.VarP(newStringValue(value, p), name, "", usage) +} + +// StringVarP is like StringVar, but accepts a shorthand letter that can be used after a single dash. +func StringVarP(p *string, name, shorthand string, value string, usage string) { + CommandLine.VarP(newStringValue(value, p), name, shorthand, usage) +} + +// String defines a string flag with specified name, default value, and usage string. +// The return value is the address of a string variable that stores the value of the flag. +func (f *FlagSet) String(name string, value string, usage string) *string { + p := new(string) + f.StringVarP(p, name, "", value, usage) + return p +} + +// StringP is like String, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string { + p := new(string) + f.StringVarP(p, name, shorthand, value, usage) + return p +} + +// String defines a string flag with specified name, default value, and usage string. +// The return value is the address of a string variable that stores the value of the flag. +func String(name string, value string, usage string) *string { + return CommandLine.StringP(name, "", value, usage) +} + +// StringP is like String, but accepts a shorthand letter that can be used after a single dash. +func StringP(name, shorthand string, value string, usage string) *string { + return CommandLine.StringP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/string_array.go b/cmd/bleve/vendor/github.com/spf13/pflag/string_array.go new file mode 100644 index 0000000..276b7ed --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/string_array.go @@ -0,0 +1,103 @@ +package pflag + +// -- stringArray Value +type stringArrayValue struct { + value *[]string + changed bool +} + +func newStringArrayValue(val []string, p *[]string) *stringArrayValue { + ssv := new(stringArrayValue) + ssv.value = p + *ssv.value = val + return ssv +} + +func (s *stringArrayValue) Set(val string) error { + if !s.changed { + *s.value = []string{val} + s.changed = true + } else { + *s.value = append(*s.value, val) + } + return nil +} + +func (s *stringArrayValue) Type() string { + return "stringArray" +} + +func (s *stringArrayValue) String() string { + str, _ := writeAsCSV(*s.value) + return "[" + str + "]" +} + +func stringArrayConv(sval string) (interface{}, error) { + sval = sval[1 : len(sval)-1] + // An empty string would cause a array with one (empty) string + if len(sval) == 0 { + return []string{}, nil + } + return readAsCSV(sval) +} + +// GetStringArray return the []string value of a flag with the given name +func (f *FlagSet) GetStringArray(name string) ([]string, error) { + val, err := f.getFlagType(name, "stringArray", stringArrayConv) + if err != nil { + return []string{}, err + } + return val.([]string), nil +} + +// StringArrayVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a []string variable in which to store the values of the multiple flags. +// The value of each argument will not try to be separated by comma +func (f *FlagSet) StringArrayVar(p *[]string, name string, value []string, usage string) { + f.VarP(newStringArrayValue(value, p), name, "", usage) +} + +// StringArrayVarP is like StringArrayVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringArrayVarP(p *[]string, name, shorthand string, value []string, usage string) { + f.VarP(newStringArrayValue(value, p), name, shorthand, usage) +} + +// StringArrayVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a []string variable in which to store the value of the flag. +// The value of each argument will not try to be separated by comma +func StringArrayVar(p *[]string, name string, value []string, usage string) { + CommandLine.VarP(newStringArrayValue(value, p), name, "", usage) +} + +// StringArrayVarP is like StringArrayVar, but accepts a shorthand letter that can be used after a single dash. +func StringArrayVarP(p *[]string, name, shorthand string, value []string, usage string) { + CommandLine.VarP(newStringArrayValue(value, p), name, shorthand, usage) +} + +// StringArray defines a string flag with specified name, default value, and usage string. +// The return value is the address of a []string variable that stores the value of the flag. +// The value of each argument will not try to be separated by comma +func (f *FlagSet) StringArray(name string, value []string, usage string) *[]string { + p := []string{} + f.StringArrayVarP(&p, name, "", value, usage) + return &p +} + +// StringArrayP is like StringArray, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringArrayP(name, shorthand string, value []string, usage string) *[]string { + p := []string{} + f.StringArrayVarP(&p, name, shorthand, value, usage) + return &p +} + +// StringArray defines a string flag with specified name, default value, and usage string. +// The return value is the address of a []string variable that stores the value of the flag. +// The value of each argument will not try to be separated by comma +func StringArray(name string, value []string, usage string) *[]string { + return CommandLine.StringArrayP(name, "", value, usage) +} + +// StringArrayP is like StringArray, but accepts a shorthand letter that can be used after a single dash. +func StringArrayP(name, shorthand string, value []string, usage string) *[]string { + return CommandLine.StringArrayP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/string_slice.go b/cmd/bleve/vendor/github.com/spf13/pflag/string_slice.go new file mode 100644 index 0000000..05eee75 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/string_slice.go @@ -0,0 +1,129 @@ +package pflag + +import ( + "bytes" + "encoding/csv" + "strings" +) + +// -- stringSlice Value +type stringSliceValue struct { + value *[]string + changed bool +} + +func newStringSliceValue(val []string, p *[]string) *stringSliceValue { + ssv := new(stringSliceValue) + ssv.value = p + *ssv.value = val + return ssv +} + +func readAsCSV(val string) ([]string, error) { + if val == "" { + return []string{}, nil + } + stringReader := strings.NewReader(val) + csvReader := csv.NewReader(stringReader) + return csvReader.Read() +} + +func writeAsCSV(vals []string) (string, error) { + b := &bytes.Buffer{} + w := csv.NewWriter(b) + err := w.Write(vals) + if err != nil { + return "", err + } + w.Flush() + return strings.TrimSuffix(b.String(), "\n"), nil +} + +func (s *stringSliceValue) Set(val string) error { + v, err := readAsCSV(val) + if err != nil { + return err + } + if !s.changed { + *s.value = v + } else { + *s.value = append(*s.value, v...) + } + s.changed = true + return nil +} + +func (s *stringSliceValue) Type() string { + return "stringSlice" +} + +func (s *stringSliceValue) String() string { + str, _ := writeAsCSV(*s.value) + return "[" + str + "]" +} + +func stringSliceConv(sval string) (interface{}, error) { + sval = sval[1 : len(sval)-1] + // An empty string would cause a slice with one (empty) string + if len(sval) == 0 { + return []string{}, nil + } + return readAsCSV(sval) +} + +// GetStringSlice return the []string value of a flag with the given name +func (f *FlagSet) GetStringSlice(name string) ([]string, error) { + val, err := f.getFlagType(name, "stringSlice", stringSliceConv) + if err != nil { + return []string{}, err + } + return val.([]string), nil +} + +// StringSliceVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a []string variable in which to store the value of the flag. +func (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string) { + f.VarP(newStringSliceValue(value, p), name, "", usage) +} + +// StringSliceVarP is like StringSliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringSliceVarP(p *[]string, name, shorthand string, value []string, usage string) { + f.VarP(newStringSliceValue(value, p), name, shorthand, usage) +} + +// StringSliceVar defines a string flag with specified name, default value, and usage string. +// The argument p points to a []string variable in which to store the value of the flag. +func StringSliceVar(p *[]string, name string, value []string, usage string) { + CommandLine.VarP(newStringSliceValue(value, p), name, "", usage) +} + +// StringSliceVarP is like StringSliceVar, but accepts a shorthand letter that can be used after a single dash. +func StringSliceVarP(p *[]string, name, shorthand string, value []string, usage string) { + CommandLine.VarP(newStringSliceValue(value, p), name, shorthand, usage) +} + +// StringSlice defines a string flag with specified name, default value, and usage string. +// The return value is the address of a []string variable that stores the value of the flag. +func (f *FlagSet) StringSlice(name string, value []string, usage string) *[]string { + p := []string{} + f.StringSliceVarP(&p, name, "", value, usage) + return &p +} + +// StringSliceP is like StringSlice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringSliceP(name, shorthand string, value []string, usage string) *[]string { + p := []string{} + f.StringSliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// StringSlice defines a string flag with specified name, default value, and usage string. +// The return value is the address of a []string variable that stores the value of the flag. +func StringSlice(name string, value []string, usage string) *[]string { + return CommandLine.StringSliceP(name, "", value, usage) +} + +// StringSliceP is like StringSlice, but accepts a shorthand letter that can be used after a single dash. +func StringSliceP(name, shorthand string, value []string, usage string) *[]string { + return CommandLine.StringSliceP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/uint.go b/cmd/bleve/vendor/github.com/spf13/pflag/uint.go new file mode 100644 index 0000000..dcbc2b7 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/uint.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- uint Value +type uintValue uint + +func newUintValue(val uint, p *uint) *uintValue { + *p = val + return (*uintValue)(p) +} + +func (i *uintValue) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + *i = uintValue(v) + return err +} + +func (i *uintValue) Type() string { + return "uint" +} + +func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i), 10) } + +func uintConv(sval string) (interface{}, error) { + v, err := strconv.ParseUint(sval, 0, 0) + if err != nil { + return 0, err + } + return uint(v), nil +} + +// GetUint return the uint value of a flag with the given name +func (f *FlagSet) GetUint(name string) (uint, error) { + val, err := f.getFlagType(name, "uint", uintConv) + if err != nil { + return 0, err + } + return val.(uint), nil +} + +// UintVar defines a uint flag with specified name, default value, and usage string. +// The argument p points to a uint variable in which to store the value of the flag. +func (f *FlagSet) UintVar(p *uint, name string, value uint, usage string) { + f.VarP(newUintValue(value, p), name, "", usage) +} + +// UintVarP is like UintVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) UintVarP(p *uint, name, shorthand string, value uint, usage string) { + f.VarP(newUintValue(value, p), name, shorthand, usage) +} + +// UintVar defines a uint flag with specified name, default value, and usage string. +// The argument p points to a uint variable in which to store the value of the flag. +func UintVar(p *uint, name string, value uint, usage string) { + CommandLine.VarP(newUintValue(value, p), name, "", usage) +} + +// UintVarP is like UintVar, but accepts a shorthand letter that can be used after a single dash. +func UintVarP(p *uint, name, shorthand string, value uint, usage string) { + CommandLine.VarP(newUintValue(value, p), name, shorthand, usage) +} + +// Uint defines a uint flag with specified name, default value, and usage string. +// The return value is the address of a uint variable that stores the value of the flag. +func (f *FlagSet) Uint(name string, value uint, usage string) *uint { + p := new(uint) + f.UintVarP(p, name, "", value, usage) + return p +} + +// UintP is like Uint, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) UintP(name, shorthand string, value uint, usage string) *uint { + p := new(uint) + f.UintVarP(p, name, shorthand, value, usage) + return p +} + +// Uint defines a uint flag with specified name, default value, and usage string. +// The return value is the address of a uint variable that stores the value of the flag. +func Uint(name string, value uint, usage string) *uint { + return CommandLine.UintP(name, "", value, usage) +} + +// UintP is like Uint, but accepts a shorthand letter that can be used after a single dash. +func UintP(name, shorthand string, value uint, usage string) *uint { + return CommandLine.UintP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/uint16.go b/cmd/bleve/vendor/github.com/spf13/pflag/uint16.go new file mode 100644 index 0000000..7e9914e --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/uint16.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- uint16 value +type uint16Value uint16 + +func newUint16Value(val uint16, p *uint16) *uint16Value { + *p = val + return (*uint16Value)(p) +} + +func (i *uint16Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 16) + *i = uint16Value(v) + return err +} + +func (i *uint16Value) Type() string { + return "uint16" +} + +func (i *uint16Value) String() string { return strconv.FormatUint(uint64(*i), 10) } + +func uint16Conv(sval string) (interface{}, error) { + v, err := strconv.ParseUint(sval, 0, 16) + if err != nil { + return 0, err + } + return uint16(v), nil +} + +// GetUint16 return the uint16 value of a flag with the given name +func (f *FlagSet) GetUint16(name string) (uint16, error) { + val, err := f.getFlagType(name, "uint16", uint16Conv) + if err != nil { + return 0, err + } + return val.(uint16), nil +} + +// Uint16Var defines a uint flag with specified name, default value, and usage string. +// The argument p points to a uint variable in which to store the value of the flag. +func (f *FlagSet) Uint16Var(p *uint16, name string, value uint16, usage string) { + f.VarP(newUint16Value(value, p), name, "", usage) +} + +// Uint16VarP is like Uint16Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint16VarP(p *uint16, name, shorthand string, value uint16, usage string) { + f.VarP(newUint16Value(value, p), name, shorthand, usage) +} + +// Uint16Var defines a uint flag with specified name, default value, and usage string. +// The argument p points to a uint variable in which to store the value of the flag. +func Uint16Var(p *uint16, name string, value uint16, usage string) { + CommandLine.VarP(newUint16Value(value, p), name, "", usage) +} + +// Uint16VarP is like Uint16Var, but accepts a shorthand letter that can be used after a single dash. +func Uint16VarP(p *uint16, name, shorthand string, value uint16, usage string) { + CommandLine.VarP(newUint16Value(value, p), name, shorthand, usage) +} + +// Uint16 defines a uint flag with specified name, default value, and usage string. +// The return value is the address of a uint variable that stores the value of the flag. +func (f *FlagSet) Uint16(name string, value uint16, usage string) *uint16 { + p := new(uint16) + f.Uint16VarP(p, name, "", value, usage) + return p +} + +// Uint16P is like Uint16, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint16P(name, shorthand string, value uint16, usage string) *uint16 { + p := new(uint16) + f.Uint16VarP(p, name, shorthand, value, usage) + return p +} + +// Uint16 defines a uint flag with specified name, default value, and usage string. +// The return value is the address of a uint variable that stores the value of the flag. +func Uint16(name string, value uint16, usage string) *uint16 { + return CommandLine.Uint16P(name, "", value, usage) +} + +// Uint16P is like Uint16, but accepts a shorthand letter that can be used after a single dash. +func Uint16P(name, shorthand string, value uint16, usage string) *uint16 { + return CommandLine.Uint16P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/uint32.go b/cmd/bleve/vendor/github.com/spf13/pflag/uint32.go new file mode 100644 index 0000000..d802453 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/uint32.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- uint32 value +type uint32Value uint32 + +func newUint32Value(val uint32, p *uint32) *uint32Value { + *p = val + return (*uint32Value)(p) +} + +func (i *uint32Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 32) + *i = uint32Value(v) + return err +} + +func (i *uint32Value) Type() string { + return "uint32" +} + +func (i *uint32Value) String() string { return strconv.FormatUint(uint64(*i), 10) } + +func uint32Conv(sval string) (interface{}, error) { + v, err := strconv.ParseUint(sval, 0, 32) + if err != nil { + return 0, err + } + return uint32(v), nil +} + +// GetUint32 return the uint32 value of a flag with the given name +func (f *FlagSet) GetUint32(name string) (uint32, error) { + val, err := f.getFlagType(name, "uint32", uint32Conv) + if err != nil { + return 0, err + } + return val.(uint32), nil +} + +// Uint32Var defines a uint32 flag with specified name, default value, and usage string. +// The argument p points to a uint32 variable in which to store the value of the flag. +func (f *FlagSet) Uint32Var(p *uint32, name string, value uint32, usage string) { + f.VarP(newUint32Value(value, p), name, "", usage) +} + +// Uint32VarP is like Uint32Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint32VarP(p *uint32, name, shorthand string, value uint32, usage string) { + f.VarP(newUint32Value(value, p), name, shorthand, usage) +} + +// Uint32Var defines a uint32 flag with specified name, default value, and usage string. +// The argument p points to a uint32 variable in which to store the value of the flag. +func Uint32Var(p *uint32, name string, value uint32, usage string) { + CommandLine.VarP(newUint32Value(value, p), name, "", usage) +} + +// Uint32VarP is like Uint32Var, but accepts a shorthand letter that can be used after a single dash. +func Uint32VarP(p *uint32, name, shorthand string, value uint32, usage string) { + CommandLine.VarP(newUint32Value(value, p), name, shorthand, usage) +} + +// Uint32 defines a uint32 flag with specified name, default value, and usage string. +// The return value is the address of a uint32 variable that stores the value of the flag. +func (f *FlagSet) Uint32(name string, value uint32, usage string) *uint32 { + p := new(uint32) + f.Uint32VarP(p, name, "", value, usage) + return p +} + +// Uint32P is like Uint32, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint32P(name, shorthand string, value uint32, usage string) *uint32 { + p := new(uint32) + f.Uint32VarP(p, name, shorthand, value, usage) + return p +} + +// Uint32 defines a uint32 flag with specified name, default value, and usage string. +// The return value is the address of a uint32 variable that stores the value of the flag. +func Uint32(name string, value uint32, usage string) *uint32 { + return CommandLine.Uint32P(name, "", value, usage) +} + +// Uint32P is like Uint32, but accepts a shorthand letter that can be used after a single dash. +func Uint32P(name, shorthand string, value uint32, usage string) *uint32 { + return CommandLine.Uint32P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/uint64.go b/cmd/bleve/vendor/github.com/spf13/pflag/uint64.go new file mode 100644 index 0000000..f62240f --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/uint64.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- uint64 Value +type uint64Value uint64 + +func newUint64Value(val uint64, p *uint64) *uint64Value { + *p = val + return (*uint64Value)(p) +} + +func (i *uint64Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + *i = uint64Value(v) + return err +} + +func (i *uint64Value) Type() string { + return "uint64" +} + +func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i), 10) } + +func uint64Conv(sval string) (interface{}, error) { + v, err := strconv.ParseUint(sval, 0, 64) + if err != nil { + return 0, err + } + return uint64(v), nil +} + +// GetUint64 return the uint64 value of a flag with the given name +func (f *FlagSet) GetUint64(name string) (uint64, error) { + val, err := f.getFlagType(name, "uint64", uint64Conv) + if err != nil { + return 0, err + } + return val.(uint64), nil +} + +// Uint64Var defines a uint64 flag with specified name, default value, and usage string. +// The argument p points to a uint64 variable in which to store the value of the flag. +func (f *FlagSet) Uint64Var(p *uint64, name string, value uint64, usage string) { + f.VarP(newUint64Value(value, p), name, "", usage) +} + +// Uint64VarP is like Uint64Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint64VarP(p *uint64, name, shorthand string, value uint64, usage string) { + f.VarP(newUint64Value(value, p), name, shorthand, usage) +} + +// Uint64Var defines a uint64 flag with specified name, default value, and usage string. +// The argument p points to a uint64 variable in which to store the value of the flag. +func Uint64Var(p *uint64, name string, value uint64, usage string) { + CommandLine.VarP(newUint64Value(value, p), name, "", usage) +} + +// Uint64VarP is like Uint64Var, but accepts a shorthand letter that can be used after a single dash. +func Uint64VarP(p *uint64, name, shorthand string, value uint64, usage string) { + CommandLine.VarP(newUint64Value(value, p), name, shorthand, usage) +} + +// Uint64 defines a uint64 flag with specified name, default value, and usage string. +// The return value is the address of a uint64 variable that stores the value of the flag. +func (f *FlagSet) Uint64(name string, value uint64, usage string) *uint64 { + p := new(uint64) + f.Uint64VarP(p, name, "", value, usage) + return p +} + +// Uint64P is like Uint64, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint64P(name, shorthand string, value uint64, usage string) *uint64 { + p := new(uint64) + f.Uint64VarP(p, name, shorthand, value, usage) + return p +} + +// Uint64 defines a uint64 flag with specified name, default value, and usage string. +// The return value is the address of a uint64 variable that stores the value of the flag. +func Uint64(name string, value uint64, usage string) *uint64 { + return CommandLine.Uint64P(name, "", value, usage) +} + +// Uint64P is like Uint64, but accepts a shorthand letter that can be used after a single dash. +func Uint64P(name, shorthand string, value uint64, usage string) *uint64 { + return CommandLine.Uint64P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/uint8.go b/cmd/bleve/vendor/github.com/spf13/pflag/uint8.go new file mode 100644 index 0000000..bb0e83c --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/uint8.go @@ -0,0 +1,88 @@ +package pflag + +import "strconv" + +// -- uint8 Value +type uint8Value uint8 + +func newUint8Value(val uint8, p *uint8) *uint8Value { + *p = val + return (*uint8Value)(p) +} + +func (i *uint8Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 8) + *i = uint8Value(v) + return err +} + +func (i *uint8Value) Type() string { + return "uint8" +} + +func (i *uint8Value) String() string { return strconv.FormatUint(uint64(*i), 10) } + +func uint8Conv(sval string) (interface{}, error) { + v, err := strconv.ParseUint(sval, 0, 8) + if err != nil { + return 0, err + } + return uint8(v), nil +} + +// GetUint8 return the uint8 value of a flag with the given name +func (f *FlagSet) GetUint8(name string) (uint8, error) { + val, err := f.getFlagType(name, "uint8", uint8Conv) + if err != nil { + return 0, err + } + return val.(uint8), nil +} + +// Uint8Var defines a uint8 flag with specified name, default value, and usage string. +// The argument p points to a uint8 variable in which to store the value of the flag. +func (f *FlagSet) Uint8Var(p *uint8, name string, value uint8, usage string) { + f.VarP(newUint8Value(value, p), name, "", usage) +} + +// Uint8VarP is like Uint8Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint8VarP(p *uint8, name, shorthand string, value uint8, usage string) { + f.VarP(newUint8Value(value, p), name, shorthand, usage) +} + +// Uint8Var defines a uint8 flag with specified name, default value, and usage string. +// The argument p points to a uint8 variable in which to store the value of the flag. +func Uint8Var(p *uint8, name string, value uint8, usage string) { + CommandLine.VarP(newUint8Value(value, p), name, "", usage) +} + +// Uint8VarP is like Uint8Var, but accepts a shorthand letter that can be used after a single dash. +func Uint8VarP(p *uint8, name, shorthand string, value uint8, usage string) { + CommandLine.VarP(newUint8Value(value, p), name, shorthand, usage) +} + +// Uint8 defines a uint8 flag with specified name, default value, and usage string. +// The return value is the address of a uint8 variable that stores the value of the flag. +func (f *FlagSet) Uint8(name string, value uint8, usage string) *uint8 { + p := new(uint8) + f.Uint8VarP(p, name, "", value, usage) + return p +} + +// Uint8P is like Uint8, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Uint8P(name, shorthand string, value uint8, usage string) *uint8 { + p := new(uint8) + f.Uint8VarP(p, name, shorthand, value, usage) + return p +} + +// Uint8 defines a uint8 flag with specified name, default value, and usage string. +// The return value is the address of a uint8 variable that stores the value of the flag. +func Uint8(name string, value uint8, usage string) *uint8 { + return CommandLine.Uint8P(name, "", value, usage) +} + +// Uint8P is like Uint8, but accepts a shorthand letter that can be used after a single dash. +func Uint8P(name, shorthand string, value uint8, usage string) *uint8 { + return CommandLine.Uint8P(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/github.com/spf13/pflag/uint_slice.go b/cmd/bleve/vendor/github.com/spf13/pflag/uint_slice.go new file mode 100644 index 0000000..edd94c6 --- /dev/null +++ b/cmd/bleve/vendor/github.com/spf13/pflag/uint_slice.go @@ -0,0 +1,126 @@ +package pflag + +import ( + "fmt" + "strconv" + "strings" +) + +// -- uintSlice Value +type uintSliceValue struct { + value *[]uint + changed bool +} + +func newUintSliceValue(val []uint, p *[]uint) *uintSliceValue { + uisv := new(uintSliceValue) + uisv.value = p + *uisv.value = val + return uisv +} + +func (s *uintSliceValue) Set(val string) error { + ss := strings.Split(val, ",") + out := make([]uint, len(ss)) + for i, d := range ss { + u, err := strconv.ParseUint(d, 10, 0) + if err != nil { + return err + } + out[i] = uint(u) + } + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + s.changed = true + return nil +} + +func (s *uintSliceValue) Type() string { + return "uintSlice" +} + +func (s *uintSliceValue) String() string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = fmt.Sprintf("%d", d) + } + return "[" + strings.Join(out, ",") + "]" +} + +func uintSliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Empty string would cause a slice with one (empty) entry + if len(val) == 0 { + return []uint{}, nil + } + ss := strings.Split(val, ",") + out := make([]uint, len(ss)) + for i, d := range ss { + u, err := strconv.ParseUint(d, 10, 0) + if err != nil { + return nil, err + } + out[i] = uint(u) + } + return out, nil +} + +// GetUintSlice returns the []uint value of a flag with the given name. +func (f *FlagSet) GetUintSlice(name string) ([]uint, error) { + val, err := f.getFlagType(name, "uintSlice", uintSliceConv) + if err != nil { + return []uint{}, err + } + return val.([]uint), nil +} + +// UintSliceVar defines a uintSlice flag with specified name, default value, and usage string. +// The argument p points to a []uint variable in which to store the value of the flag. +func (f *FlagSet) UintSliceVar(p *[]uint, name string, value []uint, usage string) { + f.VarP(newUintSliceValue(value, p), name, "", usage) +} + +// UintSliceVarP is like UintSliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) UintSliceVarP(p *[]uint, name, shorthand string, value []uint, usage string) { + f.VarP(newUintSliceValue(value, p), name, shorthand, usage) +} + +// UintSliceVar defines a uint[] flag with specified name, default value, and usage string. +// The argument p points to a uint[] variable in which to store the value of the flag. +func UintSliceVar(p *[]uint, name string, value []uint, usage string) { + CommandLine.VarP(newUintSliceValue(value, p), name, "", usage) +} + +// UintSliceVarP is like the UintSliceVar, but accepts a shorthand letter that can be used after a single dash. +func UintSliceVarP(p *[]uint, name, shorthand string, value []uint, usage string) { + CommandLine.VarP(newUintSliceValue(value, p), name, shorthand, usage) +} + +// UintSlice defines a []uint flag with specified name, default value, and usage string. +// The return value is the address of a []uint variable that stores the value of the flag. +func (f *FlagSet) UintSlice(name string, value []uint, usage string) *[]uint { + p := []uint{} + f.UintSliceVarP(&p, name, "", value, usage) + return &p +} + +// UintSliceP is like UintSlice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) UintSliceP(name, shorthand string, value []uint, usage string) *[]uint { + p := []uint{} + f.UintSliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// UintSlice defines a []uint flag with specified name, default value, and usage string. +// The return value is the address of a []uint variable that stores the value of the flag. +func UintSlice(name string, value []uint, usage string) *[]uint { + return CommandLine.UintSliceP(name, "", value, usage) +} + +// UintSliceP is like UintSlice, but accepts a shorthand letter that can be used after a single dash. +func UintSliceP(name, shorthand string, value []uint, usage string) *[]uint { + return CommandLine.UintSliceP(name, shorthand, value, usage) +} diff --git a/cmd/bleve/vendor/manifest b/cmd/bleve/vendor/manifest new file mode 100644 index 0000000..719bd7b --- /dev/null +++ b/cmd/bleve/vendor/manifest @@ -0,0 +1,29 @@ +{ + "version": 0, + "dependencies": [ + { + "importpath": "github.com/inconshreveable/mousetrap", + "repository": "https://github.com/inconshreveable/mousetrap", + "vcs": "git", + "revision": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75", + "branch": "master", + "notests": true + }, + { + "importpath": "github.com/spf13/cobra", + "repository": "https://github.com/spf13/cobra", + "vcs": "git", + "revision": "b5d8e8f46a2f829f755b6e33b454e25c61c935e1", + "branch": "master", + "notests": true + }, + { + "importpath": "github.com/spf13/pflag", + "repository": "https://github.com/spf13/pflag", + "vcs": "git", + "revision": "9ff6c6923cfffbcd502984b8e0c80539a94968b7", + "branch": "master", + "notests": true + } + ] +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..d1490bd --- /dev/null +++ b/config.go @@ -0,0 +1,95 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "expvar" + "io" + "log" + "time" + + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search/highlight/highlighter/html" + index "github.com/blevesearch/bleve_index_api" +) + +var bleveExpVar = expvar.NewMap("bleve") + +type configuration struct { + Cache *registry.Cache + DefaultHighlighter string + DefaultKVStore string + DefaultMemKVStore string + DefaultIndexType string + SlowSearchLogThreshold time.Duration + analysisQueue *index.AnalysisQueue +} + +func (c *configuration) SetAnalysisQueueSize(n int) { + if c.analysisQueue != nil { + c.analysisQueue.Close() + } + c.analysisQueue = index.NewAnalysisQueue(n) +} + +func (c *configuration) Shutdown() { + c.SetAnalysisQueueSize(0) +} + +func newConfiguration() *configuration { + return &configuration{ + Cache: registry.NewCache(), + analysisQueue: index.NewAnalysisQueue(4), + } +} + +// Config contains library level configuration +var Config *configuration + +func init() { + bootStart := time.Now() + + // build the default configuration + Config = newConfiguration() + + // set the default highlighter + Config.DefaultHighlighter = html.Name + + // default kv store + Config.DefaultKVStore = "" + + // default mem only kv store + Config.DefaultMemKVStore = gtreap.Name + + // default index + Config.DefaultIndexType = scorch.Name + + bootDuration := time.Since(bootStart) + bleveExpVar.Add("bootDuration", int64(bootDuration)) + indexStats = NewIndexStats() + bleveExpVar.Set("indexes", indexStats) + + initDisk() +} + +var logger = log.New(io.Discard, "bleve", log.LstdFlags) + +// SetLog sets the logger used for logging +// by default log messages are sent to io.Discard +func SetLog(l *log.Logger) { + logger = l +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..5a8e497 --- /dev/null +++ b/config/README.md @@ -0,0 +1,11 @@ +# Bleve Config + +**NOTE** you probably do not need this package. It is only intended for general purpose applications that want to include large parts of Bleve regardless of whether or not the code is directly using it. + +## General Purpose Applications + +A general purpose application, that must allow users to express the need for Bleve components at runtime can accomplish this by: + +``` +import _ "github.com/blevesearch/bleve/config" +``` \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..492b86f --- /dev/null +++ b/config/config.go @@ -0,0 +1,124 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + // token maps + _ "github.com/blevesearch/bleve/v2/analysis/tokenmap" + + // fragment formatters + _ "github.com/blevesearch/bleve/v2/search/highlight/format/ansi" + _ "github.com/blevesearch/bleve/v2/search/highlight/format/html" + + // fragmenters + _ "github.com/blevesearch/bleve/v2/search/highlight/fragmenter/simple" + + // highlighters + _ "github.com/blevesearch/bleve/v2/search/highlight/highlighter/ansi" + _ "github.com/blevesearch/bleve/v2/search/highlight/highlighter/html" + _ "github.com/blevesearch/bleve/v2/search/highlight/highlighter/simple" + + // char filters + _ "github.com/blevesearch/bleve/v2/analysis/char/asciifolding" + _ "github.com/blevesearch/bleve/v2/analysis/char/html" + _ "github.com/blevesearch/bleve/v2/analysis/char/regexp" + _ "github.com/blevesearch/bleve/v2/analysis/char/zerowidthnonjoiner" + + // analyzers + _ "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" + _ "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" + _ "github.com/blevesearch/bleve/v2/analysis/analyzer/simple" + _ "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + _ "github.com/blevesearch/bleve/v2/analysis/analyzer/web" + + // token filters + _ "github.com/blevesearch/bleve/v2/analysis/token/apostrophe" + _ "github.com/blevesearch/bleve/v2/analysis/token/camelcase" + _ "github.com/blevesearch/bleve/v2/analysis/token/compound" + _ "github.com/blevesearch/bleve/v2/analysis/token/edgengram" + _ "github.com/blevesearch/bleve/v2/analysis/token/elision" + _ "github.com/blevesearch/bleve/v2/analysis/token/keyword" + _ "github.com/blevesearch/bleve/v2/analysis/token/length" + _ "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + _ "github.com/blevesearch/bleve/v2/analysis/token/ngram" + _ "github.com/blevesearch/bleve/v2/analysis/token/reverse" + _ "github.com/blevesearch/bleve/v2/analysis/token/shingle" + _ "github.com/blevesearch/bleve/v2/analysis/token/stop" + _ "github.com/blevesearch/bleve/v2/analysis/token/truncate" + _ "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" + _ "github.com/blevesearch/bleve/v2/analysis/token/unique" + + // tokenizers + _ "github.com/blevesearch/bleve/v2/analysis/tokenizer/exception" + _ "github.com/blevesearch/bleve/v2/analysis/tokenizer/regexp" + _ "github.com/blevesearch/bleve/v2/analysis/tokenizer/single" + _ "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" + _ "github.com/blevesearch/bleve/v2/analysis/tokenizer/web" + _ "github.com/blevesearch/bleve/v2/analysis/tokenizer/whitespace" + + // date time parsers + _ "github.com/blevesearch/bleve/v2/analysis/datetime/flexible" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/iso" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/optional" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/percent" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/sanitized" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/microseconds" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/milliseconds" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/nanoseconds" + _ "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/seconds" + + // languages + _ "github.com/blevesearch/bleve/v2/analysis/lang/ar" + _ "github.com/blevesearch/bleve/v2/analysis/lang/bg" + _ "github.com/blevesearch/bleve/v2/analysis/lang/ca" + _ "github.com/blevesearch/bleve/v2/analysis/lang/cjk" + _ "github.com/blevesearch/bleve/v2/analysis/lang/ckb" + _ "github.com/blevesearch/bleve/v2/analysis/lang/cs" + _ "github.com/blevesearch/bleve/v2/analysis/lang/da" + _ "github.com/blevesearch/bleve/v2/analysis/lang/de" + _ "github.com/blevesearch/bleve/v2/analysis/lang/el" + _ "github.com/blevesearch/bleve/v2/analysis/lang/en" + _ "github.com/blevesearch/bleve/v2/analysis/lang/es" + _ "github.com/blevesearch/bleve/v2/analysis/lang/eu" + _ "github.com/blevesearch/bleve/v2/analysis/lang/fa" + _ "github.com/blevesearch/bleve/v2/analysis/lang/fi" + _ "github.com/blevesearch/bleve/v2/analysis/lang/fr" + _ "github.com/blevesearch/bleve/v2/analysis/lang/ga" + _ "github.com/blevesearch/bleve/v2/analysis/lang/gl" + _ "github.com/blevesearch/bleve/v2/analysis/lang/hi" + _ "github.com/blevesearch/bleve/v2/analysis/lang/hr" + _ "github.com/blevesearch/bleve/v2/analysis/lang/hu" + _ "github.com/blevesearch/bleve/v2/analysis/lang/hy" + _ "github.com/blevesearch/bleve/v2/analysis/lang/id" + _ "github.com/blevesearch/bleve/v2/analysis/lang/in" + _ "github.com/blevesearch/bleve/v2/analysis/lang/it" + _ "github.com/blevesearch/bleve/v2/analysis/lang/nl" + _ "github.com/blevesearch/bleve/v2/analysis/lang/no" + _ "github.com/blevesearch/bleve/v2/analysis/lang/pl" + _ "github.com/blevesearch/bleve/v2/analysis/lang/pt" + _ "github.com/blevesearch/bleve/v2/analysis/lang/ro" + _ "github.com/blevesearch/bleve/v2/analysis/lang/ru" + _ "github.com/blevesearch/bleve/v2/analysis/lang/sv" + _ "github.com/blevesearch/bleve/v2/analysis/lang/tr" + + // kv stores + _ "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + _ "github.com/blevesearch/bleve/v2/index/upsidedown/store/goleveldb" + _ "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + _ "github.com/blevesearch/bleve/v2/index/upsidedown/store/moss" + + // index types + _ "github.com/blevesearch/bleve/v2/index/upsidedown" +) diff --git a/config_app.go b/config_app.go new file mode 100644 index 0000000..60b1db3 --- /dev/null +++ b/config_app.go @@ -0,0 +1,24 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build appengine || appenginevm +// +build appengine appenginevm + +package bleve + +// in the appengine environment we cannot support disk based indexes +// so we do no extra configuration in this method +func initDisk() { + +} diff --git a/config_disk.go b/config_disk.go new file mode 100644 index 0000000..a9ab1e4 --- /dev/null +++ b/config_disk.go @@ -0,0 +1,26 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !appengine && !appenginevm +// +build !appengine,!appenginevm + +package bleve + +import "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + +// in normal environments we configure boltdb as the default storage +func initDisk() { + // default kv store + Config.DefaultKVStore = boltdb.Name +} diff --git a/data/test/sample-data.json b/data/test/sample-data.json new file mode 100644 index 0000000..dc0bc0c --- /dev/null +++ b/data/test/sample-data.json @@ -0,0 +1,10 @@ +[{"title":"Edinburgh/Leith","name":"Ocean Apartments","address":"2 Western Harbour Midway","directions":null,"phone":"+44 131 553 7394","tollfree":null,"email":null,"fax":null,"url":"http://www.oceanservicedapts.com","checkin":"15H00","checkout":"10H00","price":"from £70","geo":{"lat":55.9812,"lon":-3.2248,"accuracy":"RANGE_INTERPOLATED"},"type":"hotel","id":8576,"country":"United Kingdom","city":"Edinburgh","state":null,"reviews":[{"content":"In my personal opinion, this hotel is one of the many hidden gems of Istanbul. Located in an area not very easy to get if you are driving yourself. I would advise taking a taxi to get there. The service from the first \"hello\" until the last \"bye\" was impecable. The terrace restaurant wiew is second to none. The food was excellent. The staff was very nice. In short, next time I am going to Isnbul, I dont believe I will stay anywehere else. Highly recomended.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":5.0,"Value":5.0,"Sleep Quality":5.0,"Rooms":5.0,"Location":5.0},"author":"Lindsey Wiegand III","date":"2013-01-01 16:30:13 +0300"},{"content":"When you first arrive at TomTom Suites you might wonder where you are coming to as the area looks a little run down! But it is in a great location in a street with no passing traffic. Its so quiet. Access to the main sites is easy. Downhill, the tram is within a 5 minute walk and uphill, Iskatel Cadesi ( a buzzing main street with loads of shops and restaurants) and a 2 minute walk round the corner leads to some narrow streets full of atmosphere and restaurants This boutique hotel itself is a sea of tranquility with beautifully appointed rooms, a great breakfast and very helpful staff. Our only regret was that the Terrace bar wasn't open but as the weather was unseasonably inclement it wasn't a problem. We had a great time and would definitely recommend the TomTom Suites","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":5.0,"Value":5.0,"Sleep Quality":5.0,"Rooms":5.0,"Location":4.0},"author":"Bulah Weissnat","date":"2013-06-01 17:06:53 +0300"},{"content":"Staying at Tomtom was the best possible choice we could have made, everything was simply perfect: the location, the facilities, the staff, the quality of the food, the elegance and modern style of the furniture, the superb view of the terrace where we had breakfast as well as a wonderful dinner and a lovely evening. Many details made us feel at home, such as the Ipod dock in the rooms and the Ipads we could use free of charge during breakfast. Most of all, the friendliness of the staff, all of them extremely helpful. I'd highlight Chiara, who gave us many tips, especially telling us to go to Bagdah st., on the Asian side, a place great place that no books mentioned; and Ali, a wonderful guy, much more than a concierge, and a perfect host who did everything he could to make our stay as good as it gets. the proximity to Istklal street, and the tram make the location convenient for both day and night. Considering everything, including the fair rates cherged, I doubt there's a better choice in town.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":5.0,"Value":5.0,"Sleep Quality":5.0,"Rooms":5.0,"Location":5.0},"author":"Ottis Pacocha","date":"2012-08-07 08:16:49 +0300"},{"content":"A really wonderful hotel in a superb location on a traffic free road. It was so peaceful it was easy to forget that we were in the middle of one of the world's busiest cities. The staff were deligthful and couldn't do enough to help. The hotel is in a converted monastary which has been sympathetically refurbished, the artwork depicting istanbul brings a touch of colour - especially in the lift shaft. Our room was enormous and well appointed with a luxurious large bathroom - and yes, a jacuzzi bath. The rooftop restaurant and bar was a perfect way to relax at the end of the day and must have some of the best views in Istanbul over the Bosphorous and the Golden Horn. The food was excellent, modern turkish using french cooking techniques and local produce. Overall I can't wait to go back and I can't recommend the place highly enough.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":5.0,"Value":5.0,"Sleep Quality":5.0,"Rooms":5.0,"Location":5.0},"author":"Dr. Amira Murazik","date":"2013-10-14 03:30:16 +0300"},{"content":"My wife, mother and I really enjoyed our recent stay at TomTom. We always prefer smaller boutique places that are well designed and in actual neighborhoods vs. larger impersonal luxury hotels. TomTom didn't dissapoint! As previously mentioned, beautifully designed and furnished rooms, the best toiletries, etc. The roof deck is very nice with a great view. The location is amazing, in-between everywhere that you want to go but far enough away from the touristy madness. I'd argue it's one of the best situated hotels in Istanbul. As mentioned, it's a very steep but quick walk up to the main street of Isklal. However, this can be challenging if you have any mobility issues. In general, the area is rather hilly but so is most of Istanbul. In the other direction, you can walk up into a neighborhood renowned for antiques and nice cafes or down to the tram that takes you to the main tourist attractions or the road to get to Ortokoy or Bebek. At no point does it feel like you are in the \"tourist district\" but you never feel unwelcome or out of place. As for noise, we found the building itself to be quiet and well insulated. You didn't hear noise from the rooms around you and minimal noise from the room above. Nowhere near as bad as what an earlier reviewer described. The only real noise to speak of does come from the kids in the morning and the afternoon who walk on the dead-end road to and from school. Other than this, and some kids playing soccer on occasion, the street outside of the hotel is pretty quiet Breakfast in the morning is great. Freshly baked pastries (done at the hotel) and fruit, cheeses and meats, served by extremely friendly staff. Fuat in particular is a delight to interact with and was extremely helpful. The staff overall is very friendly and accommodating. Everyone at the front desk regardless of time of day did whatever they could to make sure we were receiving the best service possible. There are however a few things that need improvement to make the hotel even better. -While breakfast is excellent, we were less than impressed with dinner. To the point that we ate there once and did not choose to repeat the experience. The service was good but the quality of the food and the cooking was pretty bad esp. considering the prices of the food vs. what can be had in the neighborhood for much less. We are admitted foodies, but we didn't hold the hotel restaurant up to lofty expectations. This needs to be addressed. -While the staff is very friendly and eager to help, the hotel would benefit from a dedicated concierge or someone who really knows about the various restaurants around the city. With a hotel of this caliber, we expected them to be more informed about different places and to let us know how traffic can affect getting there, etc. If you didn't ask them these kinds of questions, it didn't occur to them to tell you. -Lastly, with the windows closed in the room there is zero airflow without having the heat on and it's extremely dry. I understand them not wanting to run the AC unless the temperature warrants, but there should be a way to circulate air without opening the windows in the spring (see my earlier comment about noise from school kids in the morning). In conclusion, we wouldn't hesitate to recommend TomTom to friends and hope to stay there again the next time we are in Istanbul.","ratings":{"Service":4.0,"Cleanliness":5.0,"Overall":5.0,"Value":4.0,"Sleep Quality":4.0,"Rooms":5.0,"Location":5.0},"author":"Marcelle Haley","date":"2015-07-17 19:17:23 +0300"},{"content":"We had been at Istanbul for business purposes and used to stay at the well-known brands at Taxim. This time the trip was for leisure and we were looking for a charming, cozy, friendly, clean, staff friendly boutique hotel. So, THIS IS TomTom suites and we consider ourselves very lucky to stay there. Due to the Italian embassy its socak is not crowded and safe 24/7. Breakfast variety was very satisfactory, view from terrace wonderful, staff very friendly, very quiet and clean. If you are looking for a hotel with ID, we strongly recommend it. Be aware that Istiklal street is only 5 minute walking, but uphill.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":5.0,"Value":5.0,"Location":4.0,"Rooms":5.0},"author":"Peggie Little","date":"2014-04-22 04:05:24 +0300"}],"public_likes":["Ms. Braulio Kuhic"],"vacancy":true,"description":"Modern, stylish contemporary serviced apartments 4 miles for Edinburgh's city Centre.","alias":"Serviced Apartments","pets_ok":false,"free_breakfast":true,"free_internet":false,"free_parking":true} +,{"title":"Edinburgh/Old Town","name":"Euro Hostel Edinburgh","address":null,"directions":null,"phone":"+44 8454 900 461","tollfree":null,"email":null,"fax":null,"url":"http://www.euro-hostels.co.uk/Edinburgh_hostel/","checkin":null,"checkout":null,"price":null,"geo":{"lat":55.94825,"lon":-3.18805,"accuracy":"APPROXIMATE"},"type":"hotel","id":8661,"country":"United Kingdom","city":"Edinburgh","state":null,"reviews":[{"content":"A plain and simple hotel, located on a busy street with many company offices nearby I can't imagine a tourist staying here. It is far away from the city center, and anything that a tourist might be interested in. In general, you will need a car, or a taxi to get to anything unless your business office is within walking distance. About €25 from the airport via taxi, this fairly modern looking hotel is very plain and simple in desgin, layout and comforts. Functional, and at a rate of about €85 per night - it was almost a bargain. Surprisingly quite given the location on the street, my \"no smoking\" room was clearly smoked in. Upon complaining - it was explained to me that the room was in fact no-smoking. No offer to move me, no thought that there might be an issue .... who knows, perhaps they had heard this before. Breakfast included, a simple breakfast that was OK in general, and nothing special. I would only recommend staying here if you are close enough to what you need to go to that you can walk.","ratings":{"Service":3.0,"Cleanliness":2.0,"Overall":2.0,"Value":3.0,"Sleep Quality":2.0,"Rooms":2.0,"Location":1.0},"author":"Rachel O'Hara","date":"2014-03-12 14:29:07 +0300"},{"content":"We (2 couples) recently stayed at the Hotel for 4 nights in their standard rooms -- 1 room off the garden and 1 in the main building.Both rooms were on the small side but the quality of the rooms more than compensated for their size.Breakfast was excellent.The hotels main asset in our eyes were all their staff who were very professional and looked after all our needs exceptionally well.The location of the hotel is excellent close to shops sights and restaurants. If you are travelling to Paris for a short trip i would recommend staying here.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":4.0,"Value":4.0,"Sleep Quality":5.0,"Rooms":3.0,"Location":5.0},"author":"Viola Reinger","date":"2012-09-21 02:53:29 +0300"}],"public_likes":["Laila Jacobs","Dr. Andreane Berge","Ophelia Walter","Mac Hackett","Belle Bartell","Spencer Erdman","Elna Monahan","Shanelle Hayes","Ms. Wallace Larkin"],"vacancy":false,"description":"Kincaid's Court, Guthrie Street. In Cowgate, open every summer from June 8th until September 2nd. Budget accommodation in 43 apartments used as student residences during term time.","alias":null,"pets_ok":false,"free_breakfast":false,"free_internet":true,"free_parking":true} +,{"title":"Edinburgh/Old Town","name":"The Sheraton Grand Hotel","address":null,"directions":null,"phone":"+44 131 229 9131","tollfree":null,"email":null,"fax":null,"url":null,"checkin":null,"checkout":null,"price":null,"geo":{"lat":55.947,"lon":-3.2073,"accuracy":"APPROXIMATE"},"type":"hotel","id":8662,"country":"United Kingdom","city":"Edinburgh","state":null,"reviews":[{"content":"Really loved this hotel, it was beautifully decorated (very much interior designed) and was spotlessly clean. We had a booked a superior double and when we arrived we were told we'd been upgraded. The room was fairly large by Paris standards and had a day bed, which I assume could sleep a 3rd person/child. The bathroom was really modern and had a bath and large separate shower with large overhead shower head, plus hand held shower. All the decor was tasteful and we were at the back of the hotel overlooking the small courtyard garden, so very quiet, although the hotel is on a quiet street anyway. Short walk to the Jardin du Luxembourg and less that 5 mins to a metro, which was on a direct line to the Gare du Nord, so perfect for us as we took the Eurostar from London. Hotel booked us a table at a great restaurant, superb food, which they recommended. Breakfast was served to order and you got croissants, pain au chocolat, bread and could choose omelettes etc. The fruit salad was freshly made, the yoghurts were the posh ones in glass jars. Would really recommend the hotel, but not for small children - there are a lot of carefully placed vases and objets d'art that little fingers will want to touch...","ratings":{"Cleanliness":5.0,"Sleep Quality":5.0,"Overall":5.0,"Value":4.0,"Service":5.0},"author":"Brittany Ledner Jr.","date":"2012-06-21 07:48:16 +0300"},{"content":"I spent a wonderful week at the Villa Madame, finding the staff very helpful and gracious. My 5th floor room was very clean and light, quiet, and comfortable. The hotel is extremely well located only a few short blocks to the metro, Jardins du Luxemburg, and shopping. The hotel offered free wifi, and much more surprisingly, free international telephone service from my room via voip. Breakfasts were excellent, and the garden area was quiet and comfortable. Just around the corner is Maison du Jardin, a very excellent small restaurant with prix fixe 31 euro dinners. Staff made other great recommendations as well. I would unhesitatingly recommend and will return.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":5.0,"Value":4.0,"Sleep Quality":5.0,"Rooms":5.0,"Location":5.0},"author":"Lauren Ortiz","date":"2014-10-01 14:04:03 +0300"},{"content":"My friend and I were backbacking through London and Paris and decided to splurge a little on this hotel. It turned out amazing. You really don't want to risk a hostel in Paris, they are serious dumps. I did a hostel in Paris in 2007. I picked this hotel due to it's proximity to nightlife in the Latin Quarter and it looked nice. The bathroom had a walk in shower with glass door. Flatscreen TV had an aux speaker in the bathroom to hear the tv while doing your thing. Beds were very comfortable and the room we got was big and facing the street. The street was loud during mid afternoon due to the Catholic school playground down the block. We booked a room with two singles and when we went to check in we were told they had inadvertantly given our room away. I started to freak because I was sure we would get a fast one pulled as usually happens in western europe. We were happy that after my face of death look we were upgraded to a larger room with two double beds. Unfortunately for the poor lady who had booked the room they bumped her and I don't know where she was then moved. Overall the staff was slow on check'in. Breakfast was carbs and coffee. They have a person dedicated only to breakfast and coffee however it seems odd because this is a very small hotel. We were the youngest guests in the hotel by about 50 years. Everyone else looked like they were late fifties to mid seventee's. We are single thirtysomethings. I would recommend this hotel to anyone who wants a peacefull nights sleep in Paris.","ratings":{"Service":4.0,"Cleanliness":5.0,"Overall":4.0,"Value":2.0,"Location":5.0,"Rooms":4.0},"author":"Turner Ferry","date":"2013-11-29 14:38:35 +0300"},{"content":"The Villa Madame is a lovely, comfortable, well-located boutique hotel with excellent service. My wife and I enjoyed our stay there immensely. This hotel is chic and luxuriously comfortable especially for the price. Admittedly, the room we had (Classic Double Room) was the smallest hotel room my wife and I had ever seen at about 3m x 3.5m (10' x 12'). The large outdoor terrace, fantastic bathroom (with Hermès products and great water pressure), deliciously comfortable bed, iPod docking station, super-fast free wi-fi and excellent service more than made up for the tiny room. Alex at the front desk was a delight as she answered our every question and was always happy to chat and share information. The location is excellent being only a few minutes walk from either the Rennes or St-Sulpice Metro stops, 100m from the beautiful Jardin du Luxembourg and within easy walking distance of the myriad of shops and restaurants on both the Rue de Rennes and Boulevard Saint-Germain. The hotel serves a decent continental breakfast which seems expensive at 18€ but we found a package that was below the normal rate and included breakfast. The breakfast room, as other reviewers have noted, is small and has only four tables for two. All of the tables were occupied each time we went for breakfast but the hotel happily served us as we sat in the little lounge area so it wasn't ever a problem. They don't serve lunch or dinner but there are two brasseries within 50m of the front door and many more restaurants and coffee shops just a few blocks away. There were a few other minor concerns that I had regarding this hotel: - The minibar is stocked only with two bottles of water. It would be nice if there was a limited selection of other beverages and also a kettle for tea/coffee service. - The satellite television has almost 900 channels (yes, I went through them all late one night) but half of them are Arabic and almost all of the rest are in French save for one German language station, a couple of Italian stations and only Bloomberg for the English speakers. Although one doesn't go to Paris to watch TV in the room, it's nice to relax and watch the news or a movie after a long day on the town but unless the above suits you, you won't have much cause to even turn on the set. - The street is very small and it is very difficult to find parking. This is likely not a problem for most visitors but is something to keep in mind especially if you rent a car. Overall, my wife and I loved this hotel. The few cons are heavily outweighed by the comforts we enjoyed. We are very seasoned travelers and despite the tiny room, this hotel experience was one of our best ever. We will definitely stay at the Villa Madame again.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":5.0,"Value":5.0,"Location":5.0,"Rooms":5.0},"author":"Blaze Williamson","date":"2014-04-26 13:59:54 +0300"}],"public_likes":["Madelynn Littel","Marielle Daugherty","Micah Stiedemann","Sandra Howe","Angela Oga"],"vacancy":true,"description":"21 Festival Square. Against the backdrop of majestic Edinburgh Castle, the Sheraton Grand Hotel and Spa combines city centre convenience with warm Scottish hospitality.","alias":null,"pets_ok":false,"free_breakfast":true,"free_internet":false,"free_parking":false} +,{"title":"Edinburgh/Old Town","name":"Radisson Blu Hotel","address":"80 High St","directions":null,"phone":"+44 131 557 9797","tollfree":null,"email":null,"fax":null,"url":"http://www.radissonblu.co.uk/hotel-edinburgh","checkin":null,"checkout":null,"price":null,"geo":{"lat":55.95014,"lon":-3.18667,"accuracy":"ROOFTOP"},"type":"hotel","id":8663,"country":"United Kingdom","city":"Edinburgh","state":null,"reviews":[],"public_likes":[],"vacancy":false,"description":"The Royal Mile. Less than a five-minute walk from major shopping and business districts, and the Edinburgh International Conference Centre is only a short taxi ride away.","alias":null,"pets_ok":false,"free_breakfast":true,"free_internet":true,"free_parking":false} +,{"title":"Edinburgh/Old Town","name":"Hotel Missoni","address":null,"directions":null,"phone":"+44 131 220 6666","tollfree":null,"email":null,"fax":null,"url":"http://www.hotelmissoni.com/hotelmissoni-edinburgh","checkin":null,"checkout":null,"price":null,"geo":{"lat":55.9491,"lon":-3.19275,"accuracy":"APPROXIMATE"},"type":"hotel","id":8664,"country":"United Kingdom","city":"Edinburgh","state":null,"reviews":[{"content":"We stayed for 3 nights in March, 2 consecutive and then one at the end of the trip. The area near the Termini station is not the prettiest, but it is very convenient if you are using public transportation. My suggestion when exiting Termini station is to go RIGHT and walk down about 4 blocks and then right again and over 2 blocks. We were travelling with quite a bit of luggage and we ended up going the wrong way too many times. We had no problems in the area and loved the convenience of the location. We walked to Termini to either pick up the Metro train or a bus to most all of the sites. The room was spotless and the Breakfast was delicious. Assunta - the owner, could not have been more helpful, although her sense of direction is not like ours in America. Just a short walk maybe a lot longer than we Americans are used to. Hoping on the #70 bus gets you to almost any tourist site (or close to) and most buses all head back to Termini.","ratings":{"Service":5.0,"Cleanliness":5.0,"Overall":4.0,"Value":5.0,"Sleep Quality":5.0,"Rooms":3.0,"Location":3.0},"author":"Hermina Schinner","date":"2015-03-06 22:56:15 +0300"},{"content":"This motel may not be the Ritz Carlton but if your looking for value, a great location and safe neighborhood then you've chosen the right place! It is right across the street from the CBS studios, the line for the price is right is just around the corner. My son and I stayed there for a week and received great service from the maids right on to the front desk. The front desk was more helpful than any concierge I have ever seen and they are just a wonderful hard working family. If I ever go back to LA I will definitely stay at the Beverly Inn even if it's only for the friendly service.","ratings":{"Service":5.0,"Cleanliness":4.0,"Overall":4.0,"Value":5.0,"Sleep Quality":5.0,"Rooms":4.0,"Location":5.0},"author":"Jose Swaniawski Sr.","date":"2014-02-12 20:15:16 +0300"},{"content":"This place is car motel that has seen much better days; the beds are old and offer no support, the televisions, carpets, and furnishings are likewise well-used, and the overall effect can be somewhat depressing. However, it does offer limited parking and convenient location at a very inexpensive rate and the bathrooms were clean; it attracts tourists who are more interested in the surrounding neighborhoods (which are quite nice with wonderful restaurants) and less interested in where they stay for the night. There was a cafe down the street when I was there in 2002, it's along major bus lines, with a terrific market close by across the street. It is also right across from the CBS studios, and about four blocks walking from an old-style diner and bakery. The comments on other sites focus on the ethnic background of the clerks and manager, but I found them pleasant and accommodating. That is why I actually felt safe there as a single female traveler (although I suspect others would not feel this way). This is just slightly above average for what you would expect for the price, but you do get what you pay for.","ratings":{"Cleanliness":1.0,"Overall":2.0,"Value":4.0,"Service":3.0,"Rooms":2.0},"author":"Mr. Ellis Heller","date":"2012-03-02 00:20:56 +0300"},{"content":"Dirty, cock roach infested, unsafe, and very noisy. This motel has not been updated in 25 years or more. It is very noisy (even with ear plugs) because you are a stone throw away from the Dolphin Expressway. The carpet and ceramic floors are filthy along with the furniture and bedding. Someone tried entering my room in the middle of the night, thank goodness for deadbolt and door chain. Photos online are very deceiving. RECOMMEND - DO NOT STAY AT THIS MOTEL. This motel should not be part of the Choice Hotel chain. Should be called \"Last Choice\".","ratings":{"Service":2.0,"Cleanliness":1.0,"Overall":1.0,"Value":1.0,"Sleep Quality":1.0,"Rooms":1.0,"Location":1.0},"author":"Lottie Gerhold IV","date":"2014-06-12 22:56:30 +0300"},{"content":"Creepy, dirty, dark, depressing. It looks like the stereotypical motel in the movies where a drug deal goes bad and people get murdered. Rooms smell of chemical perfumed disinfectant and it burns your nose and lungs... but you will be too afraid for your safety to open a window or door! We stayed at this hotel from 3pm to 10pm (never actually slept overnight, thank god!) because we had a late night flight and it was pouring rain in Miami. The hotel says it's \"newly renovated\" and had free WIFI, so we figured it would be a nice place to spend a few hours and relax. HA! NOOOO! We laid on top of the beds, just to watch tv and instantly became itchy. Nothing about this hotel was \"newly renovated.\" The WIFI was extremely slow. The main picture of this hotel is deceiving. It looks a LOT worse and run-down in real life! Don't bother with this place.","ratings":{"Service":2.0,"Cleanliness":1.0,"Overall":1.0,"Value":1.0,"Sleep Quality":1.0,"Rooms":1.0},"author":"Niko Keebler","date":"2014-12-12 12:32:09 +0300"},{"content":"We didn't stay at the hotel so I can't comment on the rooms. We did leave our car there while we went on a cruise ($5/night parking.) While we were gone someone siphoned ALL of the gas out of our vehicle. I have called the manager twice to alert her to the problem. I've left messages concerning \"a security issue at the hotel\" and no one has returned my call. I guess they don't care about security. Next time I will spend the $20/day to park at the port since that seems to be the only secure parking to be had.","ratings":{"Overall":1.0},"author":"Miss Alysha Goldner","date":"2012-01-17 00:40:45 +0300"},{"content":"I stayed at this motel for one night with my partner in August 2010. We had a flight early in the morning from Miami airport so we wanted a hotel close to the airport. I booked this through BOOKING.COM and payed £45. I have never stayed in Miami before so we did not know what areas were good and what was bad. When we checked in I wasa little concerned as there was a security hut and a guard at the door. We checked in and it was very run down and dirty. They asked for a credit card but I insisted on paying cash as there was no way I was going to hand over my card details. We parked our car and tried to find our room. The halls were all outdoors and very run down. There was a sign saying it had just had a refurb. I could not see where from the outside. It seemed that the area in Miami the motel was in was not a very good one, this concerned us a little. We finally found our room and went in. The room was very basic and smelt of damp. The dead lock on the door was broken and there was no safe in the room. We felt that uncomfortable that at night we pushed our suit cases up to the door. The shower had no presure and was only luke warm and the paint was peeling off from the bathroom. What really appauld us was the floor. I took my shoes and socks off and was walking around in bare foot. After two minutes my partner said \"LOOK AT YOUR FEET\". They were black. The floor was that dirty that in 2 mins my feet were black. I wet a towel and rubbed it along the floor. The towel changed to black and the carpet changed colour. The floor could do with a really good clean. The TV reception was very poor and fuzzy and we gave up in the end. Also the internet/Wifi had little/no signal. It was noisey outside and there were Police sirens sounding all night outside. We found it hard to sleep. We only stayed the night as we had to get up at 4 am to check in for our flight and we checked in at 8pm that evening. Otherwise we would have moved hotel. The only plus side was it was close to the airport. We did not stay for the breakfast but if itn was anything like the room we would have passed anyway. We did not use the pool as it was on the otherside of the motel right next to a main highway. It looked dirty and very uninviting. Please only stay at this motel if you have to or if it is free and you are feeling brave.","ratings":{"Service":1.0,"Cleanliness":1.0,"Overall":1.0,"Value":1.0,"Sleep Quality":1.0,"Rooms":2.0,"Location":3.0},"author":"Miss Weldon Flatley","date":"2015-05-21 14:38:02 +0300"}],"public_likes":["Ms. Jaleel Bartell","Rodger Jerde","Hanna Simonis"],"vacancy":false,"description":"1 George IV Bridge. Situated on the Royal Mile and designed by Rosita Missoni.","alias":null,"pets_ok":true,"free_breakfast":true,"free_internet":false,"free_parking":false} +,{"title":"Edinburgh/South","name":"Argyle Backpackers","address":"14 Argyle Pl","directions":"The number 41 bus (catch it outside Waverley railway station) goes right past the front door.","phone":"+44 131 667 9991","tollfree":null,"email":null,"fax":null,"url":"http://www.argyle-backpackers.co.uk/","checkin":null,"checkout":null,"price":"Dorm from £13","geo":{"lat":55.9385,"lon":-3.1912,"accuracy":"ROOFTOP"},"type":"hotel","id":8685,"country":"United Kingdom","city":"Edinburgh","state":null,"reviews":[{"content":"Made a one night reservation at this \"hotel\" without checking the reviews, big mistake. The hotel looks OK from the highway and looked conveniently located. Should have known when the very unfriendly girl at the front desk gave me the room key wrapped in a post-it note because they had run out of the little envelopes. The room smelled weird, when we opened the chest drawers we saw what looked like small roaches, we never took anything out of the suitcase. It was very cold that night in Miami and the heating unit blew cold air. The next morning we woke up to find out there was no hot water in the whole hotel, after many calls and trips to the front desk I was informed that there was someone coming to fix the boiler. To make a long story short, the hot water came back on at 12:00pm; checkout time is 11:00am. They wanted us to leave without taking a shower, the maid was also annoyed at us because we didn't leave the room and she had to finish to go home. Later that night while at a restaurant in South Beach, I noticed some itchy bumps on the left side of the back of my neck and scalp, as well as the knuckles of my fingers, they turned out to be BED BUG BITES!!!","ratings":{"Service":1.0,"Cleanliness":1.0,"Overall":1.0,"Value":1.0,"Sleep Quality":1.0,"Rooms":1.0,"Location":4.0},"author":"Consuelo Thiel","date":"2013-03-28 07:41:35 +0300"},{"content":"Restaurant service was o.k. Your continental breakfast is a joke. We were there 2 nights. We had 4 rooms which were reserved in August 2008. The last couple to get there Friday niight got a terrible room. Nothing in it but the bed. They wre given a different room the next morning but the damage was done. No we will not be back.","ratings":{"Cleanliness":1.0,"Overall":1.0,"Value":1.0,"Service":1.0,"Rooms":1.0},"author":"Ettie Bartell","date":"2012-09-04 06:14:54 +0300"},{"content":"Disappointing, after all these years... I have been staying at the Capri since 2001, when my family moved from SF to the North Bay. It used to be a great deal for us ex-pats and while not a luxury hotel, it was clean and comfortable (plus in a great location).My son and I stayed at the Capri this summer and we were terribly let down. They no longer offer specials and while there is renovation occurring, our room was dingy and the bed very uncomfortable. My 9 year old son said, Mom, let's not stay at the Capri anymore. Sadly, I had to agree.","ratings":{"Service":3.0,"Business service":-1.0,"Cleanliness":2.0,"Check in / front desk":3.0,"Overall":2.0,"Value":2.0,"Rooms":2.0,"Location":4.0},"author":"Harry O'Kon I","date":"2014-09-10 11:07:33 +0300"},{"content":"Best Deal in the Marina If you don't mind 1960's decor this place will fit the bill. It's very reasonable and very clean. Sometimes it books up with the Euro tours. Ask to stay on the 3rd floor, with the high ceilings and roof windows. I have actually heard the fog horns at night. There is also plenty of free parking right on-site.This hotel is right off Union street and within walking distance of the Marina Green. Tons of great restaurants and clubs. One of my favorites is the Brazen Head, which is right across the street. A small English Pub with great food and drinks. It's hard to spot but there's a real small sign out front. Also, El Canasta (sp?) has a great steak burrito.","ratings":{"Service":-1.0,"Business service":-1.0,"Cleanliness":-1.0,"Check in / front desk":-1.0,"Overall":4.0,"Value":-1.0,"Rooms":-1.0,"Location":-1.0},"author":"Sibyl Lind","date":"2014-09-20 18:19:21 +0300"},{"content":"Bed Bugs and Ants The ants didn't bother me. It was the bed bugs I detested.Before staying at the Buena Vista I didn't know what a bed bug looked. But the spots on my arms that looked like flea bites kept appearing after I got home. So I googled them.Yep, sure enough! Up popped a photo of the same type of bug that I had killed after I found it crawling on my husband's pillow while we were staying at the Buena Vista this January 2008!I ordered some all natural bed bug powder to dust all over my house. Even my kids are showing up with the spots.When I called the hotel after I got home to tell them about it they said they would block off that room. But when I called a day or two later there was someone in that room. It doesn't matter if they block off and treat one room. They've got to do the entire premises.","ratings":{"Service":5.0,"Business service":-1.0,"Cleanliness":1.0,"Check in / front desk":5.0,"Overall":2.0,"Value":5.0,"Rooms":1.0,"Location":5.0},"author":"Deshawn Rippin","date":"2014-10-17 15:46:51 +0300"},{"content":"Not a great place to stay This place is falling apart...was once a nice little place to stay but not now. Lobby was shabby and dirty, room was not better...there was mold in the tub, no movies here to buy on cable, iron in room was broke....etc. For the $160 they charged per night ( with a AAA card), I would not go back...there is a very nice small, totally remodeled motel two blocks down that we should have stayed at for the same price and most definitly will next time...It is called Hotel Del Sol...check it out, thehoteldelsol.com....much, much better for the $$$$.","ratings":{"Service":2.0,"Business service":1.0,"Cleanliness":1.0,"Check in / front desk":3.0,"Overall":2.0,"Value":1.0,"Rooms":2.0,"Location":4.0},"author":"Wayne Tremblay III","date":"2012-10-27 01:48:16 +0300"},{"content":"Great Budget Accommodation If you want a small budget sized no frills hotel that offers a resonable level of service then this is for you. The rooms were large and the housekeeping very good. The front desk service was always helpful and friendly with good advice. Traffic noise was not as bad as expected and nor was the beds. Only minus was the continental breakfast American style which differed somewhat from what we experienced in other countries. Too sweet for our taste.Overall a great experience and well located although being closer to eateries would have been appreciated. Can recommend the Liquor Store accross the road. Their staff were great!","ratings":{"Service":3.0,"Business service":-1.0,"Cleanliness":3.0,"Check in / front desk":-1.0,"Overall":3.0,"Value":3.0,"Rooms":4.0,"Location":-1.0},"author":"Margaretta Miller","date":"2012-04-19 20:46:49 +0300"}],"public_likes":["Narciso Wiegand","Graciela Bailey","Kavon Bruen","Aditya Feest","Caleb Medhurst","Ross Rippin","Germaine Kunde"],"vacancy":true,"description":"Two good self-catering kitchens, garden, conservatory/seating area, choice of different sized dorms, and private rooms.Definitely not a party hostel.","alias":null,"pets_ok":true,"free_breakfast":true,"free_internet":true,"free_parking":false} +,{"title":"Abbeville","name":"Chez Mel","alt":null,"address":"63-65 rue Saint-Vulfran","directions":null,"phone":"+33 3 22 19 48 64","tollfree":null,"email":null,"url":null,"hours":null,"image":null,"price":null,"content":"With an old style setting and musical accompaniment, this is a hearty and family-friendly crêpe restaurant. It is also a tea room in the afternoon.","geo":{"lat":50.104437,"lon":1.829432,"accuracy":"RANGE_INTERPOLATED"},"activity":"eat","type":"landmark","id":33,"country":"France","city":"Abbeville","state":"Picardie"} +,{"title":"Aberdour","name":"Aberdour Castle","alt":null,"address":null,"directions":null,"phone":null,"tollfree":null,"email":null,"url":"http://www.historic-scotland.gov.uk/propertyresults/propertyoverview.htm?PropID=PL_001","hours":null,"image":null,"price":null,"content":"Is a fascinating, 12th-century castle which was granted by Robert the Bruce to his friend and nephew, Thomas Randolph, Earl of Moray. It includes the beautiful and well-maintained castle gardens, as well as a spectacular beehive-shaped dovecot built at the end of the sixteenth century.","geo":{"lat":56.0552,"lon":-3.2985,"accuracy":"APPROXIMATE"},"activity":"see","type":"landmark","id":35,"country":"United Kingdom","city":"Aberdour","state":null} +,{"title":"Aberdour","name":"The Silver Sands Beach","alt":null,"address":null,"directions":null,"phone":null,"tollfree":null,"email":null,"url":null,"hours":null,"image":null,"price":null,"content":"is one of Scotland's seven Blue Flag awarded beaches, and is incredibly popular in summer time. For those after a bit of peace and quiet, the '''Black Sands Beach''' may be more to your tastes.","geo":{"lat":56.0544,"lon":-3.2863,"accuracy":"ROOFTOP"},"activity":"see","type":"landmark","id":36,"country":"United Kingdom","city":"Aberdour","state":null} +,{"title":"Aberdour","name":"Aberdour Railway Station","alt":null,"address":null,"directions":null,"phone":null,"tollfree":null,"email":null,"url":null,"hours":null,"image":null,"price":null,"content":"is a beautifully kept and cared for example of a traditional station, and regularly wins the "Best Station and Gardens in Great Britain" award.","geo":{"lat":56.05471,"lon":-3.30089,"accuracy":"RANGE_INTERPOLATED"},"activity":"see","type":"landmark","id":37,"country":"United Kingdom","city":"Aberdour","state":null}] \ No newline at end of file diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..0c55551 --- /dev/null +++ b/doc.go @@ -0,0 +1,37 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package bleve is a library for indexing and searching text. + +Example Opening New Index, Indexing Data + + message := struct{ + Id: "example" + From: "xyz@couchbase.com", + Body: "bleve indexing is easy", + } + + mapping := bleve.NewIndexMapping() + index, _ := bleve.New("example.bleve", mapping) + index.Index(message.Id, message) + +Example Opening Existing Index, Searching Data + + index, _ := bleve.Open("example.bleve") + query := bleve.NewQueryStringQuery("bleve") + searchRequest := bleve.NewSearchRequest(query) + searchResult, _ := index.Search(searchRequest) +*/ +package bleve diff --git a/docs/bleve.png b/docs/bleve.png new file mode 100644 index 0000000000000000000000000000000000000000..c2227aea27728a63536b64cfb2f57e0e7bd81040 GIT binary patch literal 6727 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 of Docvalues

^gnqr=qagl-608r%)P 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 z z;#2C*Kx8Q|8K@G&G937AgwJ}6m;GGl!j+WL!B-Q<7jqs?pm75?0+=jpE6KlaY%5!) zE%d$-LqK>nCgh79CX)fa!;>w6jXyeK{i7Rmweacj^?1dxBO|P-g~3F$!hW{6{=nS2 zw6!K|9C$#b4iaUt}->glI zM`30HVm6aUU}!iq;cu8v=4n>|XU~iS2thGb=X%u(IJ(bR5+uv7q=C;*Kj(Y5_u%f* zZ>0K88nmB9;J1cq`|u}IvFFBb%+>w*+k1So&m}P|H9{eC20`DFhZg`-QZNC#t$4L z%=0kHd8=}G)Js7E_Hs~1ruvfY+F3-qb|!))n=-Z(Nryh!&hC0jBRm`SCqSW!=Evz5 za&~1sX}0}sbF*>sjD52XSr9}$5e?pLo^Ea|fWM!FUOKn-o5lGLU4tj&nw*RThwjW+ zSIhvqI`&E8GbS?}tLS6CxteM}id>35UhVe1f_sPj zJtWQlM@CN&{QvRslmyLo8_n1v%;!se-aUxqGT+zD6ZFz>4(r=9^mubPtB2tLX^@~7 zB?4Y_2!(2?j7NmAEv+&%Glhggh}t{YzbFh3_#!#Uc#Bldnf3~b2f|f=N4|auN1PdpcI6`!##}W*T;w@+bD?r zP%bhOrHv_z*qAa=bbHJ}ji)2n!cJCWtOLyqSYoe*o;9idbc(TzOq;vE|`W(bl;vC>o5}ba$GOd*p(k*3O}U2JXI{;j=nsZSfdV` zn8NS9a@ntsBA7NO0zP_t@YN%@5M^e!^pLeP^1bP(M$iY)@GDo-5b55bsx3L+w%K?B zSo^Nf>0FyTIY+|gg@>ne1J*kPtSxN9R3bUr;5q6!PKL~3GK3VV3JgMKJgkFJXzQEj zroJg!`evukey-h{Rw7eh%({UHR`L@%j6cGSF7sXEE(Hj$sL&Jk#7F>~A_!qQ0Dhut zbOBAEt?(O!RjcTlSdJ1Fcgc0qGg~^&!;-2GDnnmRl@gPuG+6c&OlZ8pFLTq}*OzRv zJYyE6u}G5>s?WOO+{fY1Ob1QRxzgtrOUbZ(P)I8ty)OWh?0q23JA?=&><$J5mT_)| zu>D7f&_EIcJ()l(#=@BEej&gymJusWNCMtakdDQ|e039~{(;m}TT)U}8^peVq$ zdjfc)s8E@~g&rhhK#HnHj7+by7`JmV6-K|p#LpC2p(;F!`iZ51_KKHzu&fY_xwV2} z5<#0yB~UyHdPwgIUeCjJOkPjGu{w3Jh>bJ~!ycHJt>3yTgp|mqY*P{n{7C0og=tLY z$k1nBk(%qLr`S%am=jI>iF_CHe1#PW=8B)6$YMSIu*NSYG!7*W={cU7^iFf92TXj7 zeCUJ^F;NsY_F+{#)_fNaIPq=A_CQ?jm?3I^*^ZEBm%ZE01rxT%?8svNCoUB?IvKY78P4tJ|En){%l2vtnn$-$YHF@2pA8?b{`G5aX@f z4&_|&fm>N^XpqmQKScG#+9WglY zLJptn+!+<2IyRC)pDbFNZfuJrWTr^KZfuJT>c$q)mOA=+Ifqs1$QN(ytLxKDld-4* zjdWPu8+1jbf$$#a_|vk=6b1RC%9HwpxoC-aCb!}RVMX(E$#c4lY03{;T$~Gbei<<% z`VB)iq&_HwoQN{07{+01ySn%W?GU)aGvZD%K607HIn+>L;xk86zj;v~uS4UC?l?|W zTa@dm!k|qsTg51r@rg&0JB)D?*K{VNk&8aq6oYN0F!bDcH&_|U?pzLy_YGP|g<$2w z<^7lQw@ZNa5aF_gh|WLXN*!m~2Hf4An7MvdJ4*v|rV7Jx=ZfMcZ24=_;LBe#SNt0H z;VUc?W@)hIlfCvWHgssjtT3hmwt34mzrPi~CyLykL%wD%yk-rZ_k#YGjd=?FT15?o zG`{eb7`*L7{-$EmWwK1WZ8?gV?6DzX9K*O)ZPd4(?Wv8#V1M9PirI3x0QmUiI2C;e z`9661E^(nrB__sTqn#g@gJvbnd^XVd@V&Z1B<@Ka-OIunjD7`|_ltxtjYJc5T*)hD z@$mykUh-Fc(ab~TpBQ9TROALSFq_GELp`4B7o58Y3N5$s_NJ6E~*hr>HE^)8+*$$!fVoqMg)0$k*a$(Qs+slrE@$&geI}QOl*r zy?`^FWh+n0Y+32{X^vi>5}ZRHF#9qn$!H~|7(@7EL6^A}0))kyQT!e)X;FY{`8%Am zAWlX}TKU?XIw5c_cm_Q<$@+lY8uQ-zsAdJe2t8=gNsh4}i`b7b_IoCwJ(@o-1{E@o z{tSruUBaY|+WH2abwGc4t9`o0bxgk1>xHMl7bptLzSfIb8!F_ovo?)fM!Dn+PZB>$ z4)7~o6cE{sg|eM)85UwXs3dzAYcT{Yl>pkaRj8=Odh#g+`d?-s$(34sri2-e)90Gv zvs8M}6H7Oo6HWx%1Hn?xXC+&y&r21xI>xqqvYe^#F1|$PjmFaDrNG79hY^bd@N|y{ z{7p7qUZzHtzm_E@SCZzDw$)e(UKU!$w@Ss(9&SK?TT-5HX4#esGn+ZW+eXc!ZGk44vGOSZ2}%?m{X z7ShmL7TfexzyA5(vdoLfEaN#5vq{WK+?-OtXTn5@3ue>4)H|Q{Q-udXZ6}wdgQ#^nyV9?lw)rr(2cP9CNnhh^)VW=<9xxC&Ky3o&qNb=oyp>KS-)!2sshs$nN+1l9BomC~_1YnJo}M@4qVX(rqb8_j#~=yYHjBN!0~7`Fs>1YTGbLrml|RW1@IDb3Q-M44t!vGBg#y#kYWF zOnAJ+FS}MwXzxPa(Q)*@YR@7t{1`W0Ds&au2vCuU!wtHEz4=k>L(oKsV!hi-4@^oa z^&`|oUX&l(}d;KAfG^T zX041xNo5v<Xz_GrtmKeL(_I{};j%hhaEitNw#>-X_A{)~JhL;KD?-Sg_cd|zmd&yekCrIb8ACnV+Gxjh{>eiF@YtHxgslxN! z7~(x^K;9>JI+SEcOPMZ6mosgl(+>#pALAL}3Bhq#gl>A#7kQzZ#_Pk}v?#I&5hQyY zNgc|Z>i&K?8lP}{7ysiDqH(<3AEO^OL(CO*Tp~0r7|;wiNWzg|_DGB%aR_qqh~QsIc+`u(fAG*Tu=G#JLj~KI-dETQPk=+T zVt`gWJ3-Kx1+_LI0AXIC>aW8{gE#w{Ovb%a3-iX`dV`~f-pNOG`eP;j4a>PggBqQ^9yC=DJ?1E}xC{ng`w_(2mWl=_K5Z)9b!gBInbn5*X-b18v<& zS2_l1_HZzSNf~Eye4Pn?-!pA}=0E>qBJ1>tny%G4v&>h)re?kRfhw={i685^CzLBC zWs=h9({?l_Jn|pfRo066x6-tgEDMo7geT9B>@4(^?Z30$q^NN#>sPix)d3g#0mC~0+gHap501-k8uH? z5U;enMZp|xt&BLbT>XtDz0j6@x-@cKUaLRI(LT%M_1;4^|50_9r3QvKp}i)(`Sr|u z#kH#0}Rd!sqgcpL4VoKTBxB*1PPX`Wp~BD|fVo_~PP7LU#2xkak!I_s6mi0U*h z4aIq*dD_=)IMuHCCYII)PE8uJ|CW(RDZMwo_LXN`(y0IW|Cg?NPs;D>zNAF`^Sn({ z204XO+tgI&nhxd7txHo~)lw@dI!*PuCEbY6`giIn$=gbX|_1l+zi~^olyE1-9|JAv^4Om0!+{7>|=r_|MHh z{o^m6|3&`%Q-Uuq)v)I))qRnA6DYPXFJ|%Cyo~&3Y4k$#)hzV+s&^<*S8vmsYhh|Y zL@+KOA}m5Of$M(hg|hY{S9xY``0P8fD$3>Jb938t6hl%-uDo8+kV!ZF@kROud{sB? z-0<1lO}j`SESO3_@gN6BK7pw)&&N0I=aElaYr4ul-a$9obZIEK(-kNijnY{pCim4f z$FQ$%JnBqRs(tCzwTnD0l>oIujX6SL@sP*pbQvNT$)6$bHXwT7$7=yrR{J_Nh_gJ- zBi@wRjJWLf=05FnV@-I|^E=|+Tft#aov0aoCJ-jaD0H=i?P(A8C?w6ZvA~(sN84z@ zdG4OjCKri*(ttvfBOhFSUdheqseXq|XFrcEF;= zV63`!e5I`}xd3Ix-kS+o)o=_!jmEhN?l)e8$#1BA8M_%@>H%f^5<`6N3obh~WJ7ty zUxT+J+DCiYucD`C06%T3`Gma+S=nh9%(84*U@`pcZu21mWTe`_kHhn+Cbg-iheb-- zos07dzRI7Z9ipEAzH2*yHjiXXV_UWo`q`y& z{NG8O$VEg${(PFerL~!;Oim+9VNR!+7HBu81-j#Dv0rcehbKY)<*!MeQ(i4pq37Jl z`{76=hcj}TVqLrm%i;}hM{j0wFwta1x|v7*GgNxvHh~1vgh}9*iRQ&?xsHmZQ4&FK z{k+SS8u&S!a@EYL)ira_<{Ho#=_uZuBJM^hy>K5E)?^ksvd~*18lzdRWWe%K&%UC> z(2B>Jez;crXOO?F$ndqwf<%qQO$F{}lK74Oi1`+HL+Axv1Xd zPi{4CzkL4J^fxu@DU<&EOG+4=0Bd3XE6I6pPz#PnN!B(Ty=?L$nsX$ZH#tUoL_Rs$ z;`Vmgd6q6Ve@lO>haG&+TL`<$oNoLnwS+YK>R>vllX;}B9&EX?qg}a6M@!(&PFp1j+-3@d$F!7_{fGPwi@cwSd65(TNW|?3-P68gHX_kk%&rdh zTnsT3?Dk2n!QM`(JoUYHVe4`SN&9bUDD-dt^~?H5|Ehk$#30NoQI8S5gbVT_EI7yK zq1!$mzUgxplvTdZ48$`XiI9>6p=c?@yB2u?R#z0}@-C3dTb8VdK7y8^Xe>Tb;4xnz zU_Rwa$O{c2oPcDU9k4R6Am>5QL)VdiXE7=UOpKIh?U>Un1h*B#iAR2^HLZ`-H0z1< zkL~PEJ;>8?e%0nKNTr3$jKy?=tf!wI?m@&+1_N~7O#DziUI6v+@(%mF&Dye@M!kzo zSQF!I<7b5yp_ogIeesAeWXT3cLnOfIwc&jIWAm@JxN`H@`brBlOSzj6WUgQhVvza@ zqem!|i`3!ewuxkUsL@X~Y8^+Sbq@@D8HQkdVFT_x6hd$sFJ)V=wL=8290|haGa)2TqJ4FOP% zO~G%O4&6(pMlQO)dvH@iuD=E%_cAdmQ6bQ&`lWx-%OAQ zvIyBh_=m08F&@SGV{m98BDi3|6vES+jKtC)VaNAv%hM;e)tKEVCsH$*{w3AsFaMB0 zUfuuvmw!z<%wLiY^Ow)*f9g<&Z}k7ne=7bfL@j^G-veE5av_(u>B>g0bnW%h$W?l^ z;r$m}(W5~Z{W<@-FI=zn!>ZaCQ`jD3O;R*=l(2`$1Dg3~vW zefUSmp67Lxl|gQ~KP$oq!9R^+bxxC!mPp#XC4b-eUh7{NuQrX4K+d5gn)g7G-ElX= z+?$aMvHa}#O9oWEMe?YRkE{)oYz3cyG*tYIcJ(CgnFKdrX_)ZPUT8egjJu7%7=|V+ zGg%!V+>%l|(n;4NpL0F(Dc55@<9d`6u2U0#uB;f5Y$F@0(P7F`35)0XwI-*D|MBVb ze~$|}USvjR@m1FBBJlG*SNNmw8}ni~p8vMhf6#&t(cavPg8O+y zRS0{;kre^!v`5g3?5=1HqiRU`hLw8~XYa%STU|=jyIic{7GS@?7Cfrig^G(OaR*hl z&}0eb!)#TC@|*}QYb^f=cWgk6$}r0wqrcOCE=YfulJs{eN`JSq^mjcjeY|9^sh@xQ z({F@*X_;jI^u^|YMtVVArCVCXKV!T0Yg;{4`rn=u=~#g?oP6rdO(Wv~rgI zbQP0zHXOXzLp%P> z@n9Y{>!_KaSGpFCK*7QD*e;c?vm^w|GlIg`7dKlbFm=`8pI2;)x{2BI9E8fA-AmX) zMYyfZYKvi!6q$MYS&<^d;2DF?#Fz-_xu=9@EfE-%0psS~njs>K4+cj~mhv zS>!-j=vJ#}Kz6lIw>Bp$V1&#lpkUaRf-_jkGC+(5_C_GG z40(WZ8|gR?2rE9mr^j}d%yi2`y^E$Wq9TO@_p3LgDSTTG){o9WDu*nhduxBFH0p0x z(rO8UExF`+?76yX+uv8RW&pCDN+e)i0MO|dkhP++IH2XeOj3Ga5M=+rlTHSvDvDkQ{_>atDH*q2EKq7KsofxLZm`r2^fMP^egB z9P7Qe-^GEb?q}J@@txjJ;*$HI)vYB6gF#m{A-q;9VOA!wx$`!1!P6;5K)R2B^l1d7 zPa;6800LBmNG?-)FO%A4*B%@c^l_yNX&9(jq&D4E16Tg!aV)ojy{cFS(JG!?w6tdi zva59Z1MV4x?T&f_E&?75_D4N?Nx=Ln1805}JQ(vL->gHD8$R}`+!N)HVO=8*I}$*t zowLyr(l-bX3V-EST3bTRAFQ+pX|SuJARwK40UIVQhOXVsGTv>$g2qncb@>8pB}PL- zO^Ab|(7o|C`gXf7+wDHK+kMe)_r8zQ`@T(ID8>Y0mrJi1zzz=-wU{@tJh|RaZv`b5 ze0U`X0O2K+P?x4{a`DinSYue5+gr`ywY(zIxWc)F{0qBU;Mwy7bJAeWn9tPrbV-N< zzO+RBa;a1=w<%{fxD%<2f`v{FO={0GgT1di;Hp&Z=l1KX#n1NI@uT7=jvGP1T`efB z@Z8gl2p->y<)V;-MA-n9(VO;t z2D`l#?RdeTBmLsyQ<9l;I9rx*6T+WJZA*ephdOyW$oOE;4W4Q6N?!m$^#}9sX^`u! z(r~5`|8Q21pZykFFj36d41UUHDCcYjKWPK!9c|8WU|ql|YFWdLSNN-QDns+sv>~Z7 zjEX0c=`Biw&B*5#jT;qKxt<1Btq-nT572#C!f0lY`&e`gGRSQ#BH@B&g3}Sndh2jZ zLhWeXewGJ%^-tnsFP8|5s5sYY_F;sDk~XBa>3FXv$1F~YkcLU0y_oR1AS$m*K`SMr zP(=>dY-ps1t%w=A6E|sy7jFQj5y}uUxv4WtuzeNNULqNuvfqMscyY3B_=A-k?q^T5 znLY8A#`KMg?KfoeK-}84q>7Z%Sna}Q(IwM(^cRw*5%1R%UlP&}>Fzy?Qrsh%V2>p1 zE4Sr!KyH^5K#eM|#1Vnbh|yt1(wRjEmZScx6izB;RCgS95OE74O16sup_|P`sB=k# z7H56!w{rmw6YUw>eE(>LaI~d)kZsPDfwQ3XG2h&D+Hq`hEqYj|%E=aEgo97qH2Fb+a0Av|;Di$<2HuI{WOTFaW7yII(2@*g_s$}|yjBgla}8RLOER?Y<0tL=$h8hPp<}`< z4l}J*7`QKu@gCO(N4GWzvJUMK&^CIMdA2Q!mV}A2;NM7gUIWr&0WFqE-wsSrh)FxgsE`AL0642H@FWh!zQ z5*L#ww~=gfb52bsI_IK#(rCpuuFs0d$5kkma4y3|_@s+v4~%x*Je3J6cWDl`EZbr$rK84E5YhUj0NMtz9TsbyzFx@c^*vFt|r+R6zEh)YZvOWHHp$iK&iJQy-!{6)<#f zg|Z0AM2rt4JoF9blm{0aY|D}eWm_2{*YU~FSD*l?(wv@P z;=nCKPEUSVGik?sdV&V{Ih|ZFc`6o_G7*OuKpd^N#L!z1QvAuU0}6`9JqTAh3E?Az zd){~xi)H%;2GSEAKZE=MTNfUF{OPYhJP^jSlv$YSC8Swv_Zbic2%5pBkIao8B2a0%Ov`j3nK~gPO`OUxAYNKb}lllazsx!7&`1h5xTi_QH zhQF-A$|`KCb&tmcRSJEn>IpJvpq`|;fN8EQTI}R9$kL$9moO9u><7@3j#xp%fGrkI z^LF8f%VG<-JTxTQH02aCx|OJHBWAzQXpX#SCKl}vj zwVJ4+`c>=dZRQOeS+WSlY+M)N$GfZ(KOYZsh{J;fD5<)B! zW6=z_R*7aeQC!({^d}P9|DF69E8Jmk5p2Id-vU+)?JVM*Q@ra3jjalNwO1XviQ3FU(tDxbyA zu`h0p{Q(fp$SgJtUWyG)fPECzVF8HXR({|DRD03oAyhdBzqq**pS=jLDp39)nX*HZ z2eSV5lC;=*59FVPbuT1P`$0-)670?JK?LZh%;0qEbJfF%;Vh>KV}0$Pa;(yt0RxD2 zw7?c0)@rKH;x++ramB@v{9DswRAoPa=HpabUv0glbwWWqaM`LZtuuZ+soHar(fX;G zHD1?g$Q=fHKQyLNB-vbFZoNjk8tLC;7He{F`9Xf?xoSZ5aaxxQzk>7ET(y14;2L&A zDq`S!?^l~BzJ>yKt%7bl!bt_mp;xEmyWYFmWXJ&4sbf`L(h`;)uAl zxx!Y*Jj_s2vNxHF7&G3Buu1tEKEU%hCOcmMp`&TW-WPyP5oeC;dV&{LRpS-~D^nT9 z?!mS3!+S(Qm1VBVfw%7OHHBdYLFXsEMKfJTx8?^`#x@-4TAsLkfD(2@Q0i4^RD$lv zhKlOI7SUMu@Z-NSgld2)0j>hL0iT zijd?3M4~!_K$s#jil^#4A(bd|Zc4~g)-fjX-q^h<9kCLKPWfA95%(^jumSUVu5(dk3ZVd~A{{}&o8pO`e9=0ii67>rB`5Yeq2n9< zLY9VS&yNPp=|Lfo-NM%7PIH3nj;2ng9DrU)b2E9}0E?knUb(FaQ`8(VRi_%r{EKp5 zO%}eB=?Xz09RLI&_nzPeXV-=T1Devydrl|Nv}bXBDx&(7Gsr|fN!^i!FiOhA?7RUt zK)Q+ym>q0Oiz{0{($MHG@X!cy#5Hp9E(p{o(SnW7=Ts_E67^o z5!SFjH8*-BmkXYnvHDdD;1jBLK#3MaSa8ws<$r;=r^aGPv582lrv1#IfnEs z0Xl zK7}WBC7Vg_^SZ0XXKmLr(fIm^mo_e5cBvFDA!g`O=-lKCI9ntm46cJxxy5dScZ_SH zfo+{Ft!2CnQXgXtn9qF5bg-2avVi0Y?^(uAS_PeD^pr){_~hmdAM1e6rgLo!DCEo| z0n^wvRTsJk1_j+3m(j`HB#w}sy5OC-E@&5vrVHPWPSUO--*o<$|559JyV(0+#o|`Z ziO@*6_`qDcB0?^m#7CB1&750+%V=>Wrp1mJJY7+3{bsKW^=J^te#g^Pxz^6AKiQYJ zOjRh3ZLSeqjnw*kRtQf`Y(|KRJ+#&?og)ykWDA#8+p>ncMFvE-EKKm;@?`PbhN)n} z`{vsuVJ!LDrEDnMCE776&z%;EJ?yk6eBLb3)L$y#YKKdc4K{sk*}o1ex^XxZ{Y$^yJn$G45zSQhTPN^ zLxRjLP*AsH>8jAdT_P>}WGdG}x#M(lE+49!*~ix9=sDFx^fGY^YE%Exm7K3~BX7Ai z@@2^EooihjJ@}v*>k7ZppBcTk^8VBNoC2)nlRSvp1{ZbVhHoK?oMWS_*%Ki<&-LJ~ ztw--$xaBq)#u>%CjHk2@{8L@{5U#HE`R)k@u9I`_sV>lcq!8JKU_RAFke=j{J_aG; zo|^MI6-D9+4&mKV?HnTSDIHw5j&PBWp2B6_!YqX~Q-5a&sWimwM|2!VGQ0QyArTr7 zBSS5!Z0Q|dl(WxY2fPr<_BMVuVZWN`IDPKUDD$6F)ck%K`&d97w^>vR?oN?Im z7|1xnuot?H5)G~5{G#y1>ZS1o_q;MJWZ z@ivqPMn4jNiS_QqUCT&DFL0qAt+_6WlxD6*Dp}^4hh-jS#+e7tY(% z6`tc5%tL4C#%qwuE;nC@7ft;O502Ikw!Y3Q)~A2aDza!I9JIf4l|v? zc!zt?gIrn@bAI1^(l_ow-*92dd2TKH*vOP_ub)}GI(vo+?%)e`K^`4HEwtn_%(ATw zMz#)%FCwH@~qg0ucQT~(IbtI~^qv$viSGtdP@!a)`g$_Z9*UuAE}KuShx&HdpkC<;)K&5MHJzwM3>3N;#8O;o%iGb7=h_7 zl3{;bs62_$xj7|%^sh_LP9Xtc``q#QmWG&rjV?d_EF8e{Y>U7!-6CZ2ptRV}5)tEL zW7jh!jJGm!_HxxGlSPX?SI^aSH(Jia-MZLkgmm4`>s>)K*8A4jvPF_Np^L|4h;re= z$LX&6n%Bcj^>YmWyq-#CJJc?)(v&Lg9bdBJOX(vez5DAYop!hrYZ1mw#Wvy!cUhM+CrtVn zBdX|^BN_4#F+TqgW7{Wz+q$dlDpcr01w`0gsAQSsOnRU0KBw%uI~Y-j>>Z}zRkF0( zYd38@6r~X-(uX@Lj&g~ySSB2sL*lk#K5*agGIx@KFT4l0`>F~#si+XdY6>YAO^AJD zY*r^_IS-to;V_gQwHG?pKH#IT!vQp${l|K8?b=uuz2LD9Jz#*g@Honqq9E>i(uI(( z!3cW~>>6iTPIi$vVRsy0GcFi$`{J1haCN5w9+=_oT9$pupfkVl3=7C31b{Z}ph&wr z#u%&^s0Q}Zh(!KwUXRW96hY>D-KI18F6B7yFZ_Jxryias1bdKCLc49vXAVpBbLGLY z;VX)=To-*FCPH0LoXZh&T>P5-W|6tfEpyB*cQ@y`i}T`Lc~fNz({t&~oYqIt&rL8C zu&~fP`vzU79A>xDZS!DDMU~7_yKnBwFYNeSmqcP%rqyD(T=}RQ%1!0s6QC=32we$6 z?3Ox~tIrbadBxK|C@o8+Qr)Zg{ogrKuNZ4tq`?I97*AjA zcF_z{x5$B)sk-71m-;Ig`K!j>70c(f*k*OD{XU9o-PMQ5*Du!^6zDPv zh_f^SEIAH33)|p6FZRX|5k^Kp-8(UBCceJouh*i0u=B;Aux^}1!e#Gku?TmCLw?C5 zxx(K03i%yQXAVtC6vjMcKf^T#BIMUtYOZ!p6hNreuQXeDZM;fl!CWS<{@Me8wiIw} zTxZBDjRTYh9}#jCEX(eb)@3%)TMD8puGh|`5=Dt_!tm!T2!-nR8h23w+~eq)Dd*9l zG1b!#h;^0O>FXyDa-)wT3J_`X74xYJ%LZ4_@fcE`+R7^`9Psp%UOP<-*HOQww|dg8 z-HcszMA+E`yNx&`E;TmhHZ48-=B|#Sm}E*0^h_@Y)9AMVqFu>ZVy$C9)_z&?#hu>; zb9IYk$j2GJklBz+Uk-Z41!UIO0HW)BWVElNbD+h`)lk5S>*Ww$O1=YGHWlETh8~z2 zFY%Z#VTUX=UczDMiGpzqd+;*6Fwc(|%A;%!e9@lW%gH-;a$QOUu5Enj$DrkHdRYcL zF*Mb=?Vns%rLGp+8T2sKc9tAL{SA|_K7e)%-*H5SR2iTgCj09HoZ49U(^c;fnQC0e zN3{!6eX?T~Q~gwT)+w)_DEaHB5Ha;rrxrc=&QmR$C_&K8Shen&to^RZ`h0E%zDF^G zfm_Y`IrqnO;yw?-Y=xrMy=JxTHLE|_YgW6xX0_ixh9KmN+V8@siw)Ry8{gi&*5p#P zn+wR;KIbQ9^V&8L)u4_%{}SE!py@?&U|Hj`O&(C@9wVL^%yMqK({!c;B@n6w<+Ret ztyE8s*s|7AW!KvI$`WRL&7xmfNJ}}*Jy)% zc{hxx3$0|dJON++6W>~WoD0OjPj_OXmZ|AF*-eI1J?aTEkcUd1t^-UM_uS)9Te0~K z7{3Y##M#?>w$_fJuh>SLQv|upp+s>52Z)C<^%zcc)NVgvZLK+rRN;*>S5uF22&f*- zqi~0+MGd0G)gpeQUOh^CfNB!hhDj8;O*LYjSg2>8!TiX-E=T@#9Qo@;?;MnQp!M>) z7}wXcE9!HZQDz+N=En;?-sv%LO%0<8d|>p-u&6fmb|}04LQMz6@ENiv}uib}`Iz#VdB%RSfYJ{`sI#=%pE6(qy z!Re+msQHr_SIpjFheWvfODRfnM08gi%jS8fJVWeqBN~R@KT0?sG3Wh+Q#@)7B4>+Y zvm{eCYS6Jc1S5<=?>Bns=+u-iizUn?``29C(wq@jSQrEjO>v(+UTHWZn& z1#}LsFV9`viDi`YGg4*+!f4ICE%%m0I|?ffmG4wF2P@XiA=Z$LLt-A{tQ_FA&x01& z`sw2~@F9i|6DS;p*4kRI=D-t;EFjb2kO8@YDY(1wNEbCQH!G&kks{BTQd+Ry1EcM1&m$bt?S#= zp#H}4Bfz+1M^tFQh0?(R5ZH<|MLWjEh1BY{$$4t)ibQgpkMpN)&FP6bQPSIgl;F0h zN<@i`r)1IbWc#~74jKG>zS|&$9Cs3$JT4~7&!8l8+O)&ooOAEVwEs-a^LCoxa+*+& zUGktT1`?QMn|vR&Y1^6}4L@+ww`qOm((OJ?tJ#c@bFQn3ongX+Fca5##??W6HCcm2 zXhkX4Od%Wg>$ISU2bZ&kW|^qM(D5{J;ZhP%SO%vA)DJle4NG$#D(Um1nH8hKM!ii6 z%^bvbUlji2QY3ZOg+dTMw#DHlg){@D+f=;L_kVwQM8Ut-?s7BM1D!Y233)?F~wyVhcM8Yw4eMhERcBC^wxphu1#a(<(k z*Y3$hE|p>UsK-Md;sIbrk89s1(ZzQ9)&$ zBO?qbfOU-Uc5(y z2?1}0B5Y0h-mTCB(oDZLA(2$oKh&t{ zQia?BkY@TG7`XK;BxKP&12jlBp=npeTwKx5vSrT;SEd(t4cX%0&M$S6oS+5!qUyy^ zHl^eWZ9a$?I%`hy9Lk%cesxB?Z)H9yi*fh@L@ex=u+uPc*}QmlIq8xsUP_ruO}{5! zzg=i%Q0qj+mQ^l(@{kEFw3jn|cBYS+r=-k z0a6=NFhEC2y4V+pnvt9xUvhw^B_qI0Y!9V9%!L13(kcq=B{xzf}wR}!}L zBOI#$;VeR#u?QVw%NRF&;&m#85&+};U|ZO-G%ziN0HN!}yZc&D=0zR4yG7zDjQ#Q6 z75l;|=?AE{DG{KKGsv!4ANA5IM!X;ex0t-Unr*Z%a`WiY4X#YCeBm4YEC-?Bhn##+_ffK{@e#gqW0WkoHWJh(*|-uXmXHk$_mkdy3`4nr^y&2yh-p)jEf>(?6l z7aJ})epr#q;xW$uWF{!Q$vE6-2gIJ-eB8(7P)%-=JY!4oOf>b`83*Oq-aa7KAyUb2 zePtZ1Ouo%wtW_4^OJQN_e2}_heEX3bZcdJjrtRhm|HtK6A#VxVd#5t-HUzw1n`mNhf+{0q>dt2jBD{&;uFbnkYrkDMBRB{= zg-K8(dw=*GIlLzQ0*ecls!!D(41F>MAINAJv>)6oKAsXNLiey_E;x z3$59aq~i;KxQhaXnq7Tae84l5ULC#jWd~mVC~u11j&Qtf`ay`s-gytd$TssVchwkQ zc2&$0ZOZ+qqi+*%eMBuMKxb$aZn!JZU>{NVG`JNtl`#`dxEU+JIGQQ^*WcOOe@tyb zg&dh%m{`?BcM77zp2>v~%-vwt6Be`~o~L?Hw}&3|$3|Qv|6O`9I(P z;m274M=h!w2ocrgVY#39X2xefBD}Nfleb}g3ZA^J(bI54^-?4sbKX>qdsxI1u}#Wo ziY-Qu2N1BkW#AV^g&rfJT#)+YZ-&`D=lnEO^T&auYkX5r1>xp$0Ucewa}yWhF5_&B?FA8)dk1cCo9osRL| zOW@`br$cD_M;}XLRrIHT*kjd7JM{F}2N2T|8@sWZV@DJ2;~Z#$h=KP((d1-2z21jp zywTb=NfwoWihM&<_;xFZbn2UQ$3Zv6*_=+rc^cr_TXibkV=|hX-iw?RE^;Is3O?LP zxUaoCQv=)#+sLK;%`Lz!mA39p(XSt;_mG1|x|L1QCRhcwbI*(dV3*DXIQq@Rr@};O6iZhFu zct6LO%`q_9Ng2VVk=I;P{EI-&$h&LuuDR2l+o5nn9njPJ!D0%Kk9+6<+LVUiyDqKJ zKG<9AAsD9}+UqtI!?5c%k3~`w<;?6kV@?Ir;zsB|%|(5@zZ@)k?5F(WU<~w*1F`ru z+)d40n_S!8DwDXYhcb8RK;~ZG-hA9h=y7+eiy>xw1nooR3gMm;qlgWnyvMGMucF_o zBG&*BGV)vf%`R?q*nblzP3AB|2fC!!Lv>o6_5;G9gC~^Epsns8AfN`0;NZbKmhpNM z1#q2-fZmCX$_ci+ugB-O9+rtblB1<0lBIJ4N6v0?nlXcp_eM8g1yjDv;!UCpWcvbp zANX`rorv|zMI6@t$$Hc|c?Vl_;eJY9In6A>=#+dWb{#vfa(4$M#?gZ8PX??fnT}mg zbp>;$`H4|`GT)r^dghHFEi8Zlc4>5-cpDB0Hbm(} zyuaL0;p@HMuF*z~^pj^{Jh+L}Qh{X|3oOYdSyUoSy^0@K6nVyGJ?$JjyB~ovv_j?x z=c_$j%(B8f#QJ0##X;8jkM-%P>P~qqic{!-R@)o6KIy*r^{I2tK6TMaz;ZALfvZ|p zcVIHR(40_jz*_E!U)`{j>v6Z+DSvoDh0U35ubSO@-K_?llPArc&`+^gi@ndaAM6d> zz2;W@k&tB|0%j*{$cd$iDa%brsCsaKHZ%kzH-Bef+Q8SL_I*!(g~l3GX0JnW9CL-6 zFzBGgYL{Z*hr0N=j&%s(=;g)~`U8+<2#{^f8Ie;VAlOzsGsDMqv-EzJ_!}G8!g4b2 zX2Rwc*#~(PId~I#EIiAy({0tWRqNg`?|)U0pHyqw6_{ zQ%5Ca_Dez89N+%<&=Y{(7rQXGUJ=Cu5m+$ft@({mh-IYGVY2ZsQ(>K0IGTDBWD$x4 zuO6?im7HM}@ecDr{MEJ{t^VrB39sL-RdxLX5&q2gI`6bef<%8fc6|-|$<|j(s8@@L zCFdl$#$sJ8@)q5@0;(ZhLI(eN<#8)2To3(8prmrSdM!(UV%fgB({m3$K~r+|jq{aT z=G;u6&7ej5S#phsqy0Q+k%+O*i6f zs@Y+aJL^$&S!p|1rM9sGhb<~_*bcU)53iiB^b$=AFQLvro-# zdT6)eIMH^0oMd`WfI$a><>En+wrk(U|4Mzi~0M4nASm!L28tCuVC{5ZMMFhV0o_ZwwT+uCCc5%E$id z{j-FG;|}MRM?0>+o1rbBA*eeyM~zHz;-VmVeuuK2H=ww?^AEWiu60V8B1By`{ka0y%F zk+E!!w@&;MKiqoT1GKp_7aJ=q*+5~5_W3jFnvk2^X7!e)PZ^zp2^q=eo8Q zcU}FFw_O3`ZC9JsXl*+BZZBi2fdE?@(q_;58#R@X5(NrbVxK}Nt3`;=u{UDXo%r0AK+PWO%lNki$?de<$bd9Up=zx)a7N$Tqx3zVH z{p1#Dh*Sp|jV_tg0VUy0fv*bOk-E(fGdJHp^i^v)2f&JgwJ3nxJO=>3=MHhvHuidW zF^lsO4*C(}zJ|lBJfhpYYL#55af|Y1wO5~THAORBsb^&i6`3eq+2CBb&QWY`<1EOo z+X_%VY2@iEj-feK@z@SgIG;nJ)lUdX2PVmI2RDnWN;~d^U^$crrjnPnYz$3M-j#9E zL>!tsE#=0!uBdR9&dkI;d5-n&$5s^aC!gJYvy~n&Ws=*_#)K;Br9mpD9CzQ2J2xX^ z_0)NtcN?se&USyVVy9$8IE}KgR+R=DYzda`@~Bfhn?D7)6{jFGc3njT-fWpmN7;D3 zjme#S%*Y4j0f;;pplk!;qIY=+wA>a6HSsOx)_c-S#?({N(l7K>#@W@V0Q{DBBE0DZ zD^)e`wk2hdo+9+XR>u%{OWoPeZ4MVPZFyjC8dwSWFXIN?O*iOnItjt)ALb!4QoqbM zZXsvdu`EeDTNc&Lw+pD(v@nJ9olY~eZ9m^YYp8467j8W;BZxBoEgobG>+IVSuYcOS zBJJ(-bDvvz?sN0!KDSTKeQxEs&#gT7xs~TWx6jUfZujRtH+SxHyF2%}*}2cHJomX3 z=RUVVRz8<79+AI8-{l3c# zTd&y$vG3#|LEStQT?>!+dsg?00BVsh26zmM=$i;$0}K&b*X&iaT;dVrq$kS|*?};I zI>$>mSj0stOI+`BN+5BSm#;(fel$M=WOX_WYyx$E$pg0#OF&S9f*6_?BE*@Ox%hC_ z?`*H={*n{?kAM1e_GU}a7uy}y*9(27%9$O&)L|ebln#ZEkhxI?vAQ-ge#B!fl_~uI z4Yf%)=x5m(=vVnvQ}|fi7Q4e-Tp7Z;i;lsRC{7QfjFs1ZxA?XfOKX?!-QzK4gj%of zcZZty&ki*sIaXcoBQUH<5zz!!-2h?7x#&tITG!o=jn8!tj`&qy`~3H9W<`}GuOzYB z;fJI!)C2XQ;>Vjj-)fQ?+;A4No3R$n%tgNj^4DO@{EQTzOakyCpGrOf(Jdv z7{7O*w12);cH)+p-M0wBd}7mnA6I%}4AC2R%L&|N381VBq7O7~-h58aM6XI92t)HI zhKKv?7IbhTK~-9ozTIWHZ!y35{xskebFI}N0B)78D7!Ky|fsK@cuO!-w4gwlrNS&ScgBa{;_JSw>4o^(K+q-%q8nOlfnh zW(wQmHe2LguT9u3ZDhQF0{I1w^0(d@h(+>NZh-!LdTvn0o-7;=a`8?@nB|H9C3bHL zV=}1WbZ1d0g>*p%>aJQ8($yJAN!jf|-DrjS95R3nDEcOM+0)e{)o-2QI$KE{V+j~5 z+8R|~zB0C6Q$2=jAQ+oQ3LxAmD1eiTe6& z0^sUNU>{LqkWN+^cr)Z))WKNYJI8XW8F)9*o9Q=a?VH-P;F#&ezR{_(EXR>nJ?bn8 z$X1t(VQgnggR{t*-ci!X7ftW~Blaer?=uSP`pD?;v>q+j;XavRlYR-pvLp!UN`fB5 z7Exy*h_GHF?hN_Q_y6aAw4aEK4#Wj&&=G%A(G@F%vG!)lb!yzfZvbs*uV;JZk5}(h zL?z8$W3K|V1-tDkNJL+}77gbs^ckGTUtJ&K^&3|OSXo@>JoiJNO2G9iw;`jjh} z&6V)0YA`_AvKDMLJnhZL!Mjk}mEbgvuYKU4t(z$%!hhmhfZuo@8c0&L{Ki+nNQU|C zs#`rdi)4oO%1RCQ8}Daf$t?0yQAn(F?y*b$RdV@^E0`qDxOJW=&H2AwSAKHwg4Nyq zeTF$I?38BFq8Na!STXdGmzkTzZAM}C39$`T0eZs}4exUp0%mx+*%ttGgQj5%9#FO| z5Mc10I~oKC_UU17N8AFB76Fq~MjCup+x!2AIw%5_+1A$OD`o{K`DRdz-!h$AT z!AMI8z;7Ew!WUe^)Y%@YlWmnHQye^PrG|;jtZf{{Y_Oy~%aptzTW%D}yMjb6<8TjiGk)B{ zFfqRV^wnK3n7*#KSK`w*|J9xRuC1p3_St{?q;HCS9rkjE;q!Yr`jyxBx-<0E8NT(? zzw!dLL40ak+jQ;ce#~#ud8+!)9D;YB(J=Ah@~Wp@y&yd3bhg8qp1x0OAf-E|oSP^y z?vtR<=v7hfM1JP`|DGcK=fN$f3Y35C|4Z8CQR9CLHNN`v8_o*%VIBW_CK=%*{O6e@ z_f6nP^?%LI{QmQI<;hojgJXlwzgj+zRS^HHFF^eK1y6OkMvPyrPIj{}$Yj5HIZfxP zz`SJPm^ET-thT6-CgW0r%$}Qb&=P}$8;@luJ*e%Rdqw*xHj(!Q{S2zG zq7&Eq@1E;+sbc1b6u7qz*s?S$=Zo@v0s`|%Q3CpYnsRIQScES0%hL@gypdEWtjP?` zFu;?I(9FVir=ccOpH*f+-c<%=)mCV8L1MlmLd=&cSD5ji6zWuf8BRH=t0FH2l_~&V z1N^voXQ%qL3$Lg29}$*dFfD$3B}DXck@|NjU)E!>+79(~sJvosiE3E@L!*6-OhE*t zDf@OyZPEGZ!`6ok!zd%;lbtX{cyoY9Ah5OZduN3ko*`;jDiyRS=lxS=GE}N^EUCM~ z@kuc%o1(rM*5#fF`LWDvLuK79lznFl2=^_(bk|o!w(LY9d0&+v{b^C7H$^@((q}eg z%N!?U>D`d`i18Va8J`Q0;$8yDpQHx90M#vVK*@aB7eMxt_7eHBMI`dRc0~Rfm1i(3 zhu=fi3vY@qR+W6?I8?7v*P^H`qabM)1+N|@2G{6XvF~BCZ4XrB8UuS|nnOX{DUAVg0wplsyYpBT9jAGz_*v8(R&S`H7(6@bu+?p! zR0oCP7$AZQ7$!<1dvzPusWHLcBP7$eUqgME-$$B@coykj?q|k zi;D)v;rlIG_zErtW+|tBmZ(iB6Gi?mtJI7`u3U;DU+S94%o=jv0=?`awLXcmJM}V| ziaKI4(bR28OvS|2opO{+y~k+!q+&0%l_=xuJFh2XnZJAZIsgA%mg?^w^4~Hy!+EK4 zRX(QyE@`C4GSBb_#{XXyRyjBN^Wz@@o_S7rUfcD#niLI{JCcF0!RIN#4AQ*fr~NuV zHy#Oq70vkyH=aK5g;%KW3qVznrJpTWjvyLm5~r1gOY} zwvP~kO#S`z9qIx0lXQe-iVo15WSX^FG+;USb~FoAq|nx0k#sc$2}unEnh+X;>jCci z#?3hEp`O}sQj-}tGnN99^b_i&o*_J_PZ?X1Go{BF2;ZCq;Z@j8u=)$#A3&o^7mn;_ z=BaMwq83JUx+nuZ>4viRkw)^NucpmBuNLL}GKDvAZ#C^mS>+DvEI?hjxlQ+e+_xT9 z{F&7gTZgOKBv>*5+IVFRkZvb=K;*Z|tZ-Zs*FQ;eyBe5~g%E@@>CO?kHqn`YA|kS6 z%m+vOs;@Pl!kNnk6&;}%ax@VdI@m|pl>YSlAJ=04;e#W7)z{8xnwnUTmt?1NGB0br z>v4u72mb^kb8_2%^PBCoooc*4WIgU)Lj%GXjNNa~U1{ZfJr-L|m~h74$KpK@=KBn3 z-F~%I@+{IFzU#5rsltRsl}$Y1SBa8fmmKB`YPbytVTg-UJ?UsBqcmBCbs`0uSYN^X z(_4Pp3A*)31-tkx%VV-5A%2LwJ}s^wo~w(BSi3!-q|<5Zc^xEx&wS~L6LUkKQPb6M ze_}Rm6~)DtWLKxRbG=x-c}e5k%!aO};c9g~;Z=@Ct*u_jg5rzb49rhj#b_wyzN5-;_s`hxejX36P>$cU!^vozWbHVh?l5#%1|;KGR1_2~UWly&Qb@g_=JXctH*3`Sa1 zHBK)H7dhp$)FDHgIaYs^IwM$EhHsKpoNx^FpU8!SXMm$S$(MY&_IHh!{sMF-5LS1t zb?s)V$-8K+w{*py_vTX(i&-D6y>?$pM53%8>nR5xyRCkn{gwML+ka;sdUd#hZ7 zb#rIaGN6oi!@YCs&F$n_Z@9Fi7lExXOIabWo2*F=UPZz@i?u)qsZbVJNB|K~sWnTQ z-2g(&LKes8YC1;%NcP5QTvrZns=H|IMYGgu6LlauS0F=Ufd|1?#em&T8NwGas^MUL zSXjaI{>p2J_5L80RF<^=(;vDi$0$Akxy;r#r|J^>5=iMDsSIx^^sV~l+W3vKN|+as z(4%Q<-#17rhD#E>G6rDWOYc88H@fzy-~%6c%!ee{*NYNb9!i7nYV>QT4G_p_P9)*I z$|Oj40ebz^2Oe^t(@vr1p@Me0!*clQ>cFA4Iy}qP*klloaR}nrltc|aSou$4 z=?GtzCbKqr#2uVP6zeHuLL@PxwZ>5IbXgfl+r^)`CC1sFQDGM{8tKDXqwB2xJ%`#p zbiIf3oLuakXS3! zw>Z$V(rcK!$;kRO0}e1^(j!HU0?EEz7~+Topd@?~Bcbtefaj}`4C%&<`uUq(&Bj0% zTm-rVVn#iK3a+#F_G;BUIA+(!>PWv8J#d2t2vCXQ{${GufIHnb*bkW{?c-!o8<#B8 zq|F1;QYDQQ?EuMw8-0vb9;NYnl7O=f(EzgNAt)o97SIw*C`6}Fnt_SXWBrJ&XE*98 z`gLsZ2irHiNXn|t(YjF}2KPGOe)$WdR=qE3PG=l<{B5$H-tuDF5{=~@bI;bbWOJii0N8SC%vL+ov61l86L3So{BnXDr%r9 z)IU=-#}go#xr)t2r;iBD*clcUyGuvc>0omQ4~TGoCrEI|4L&V`0Xcc9@tHgLLA8xh zW|j%RA`Gm>;h}L}KEUxtyTXnLU;c>D-OP-=&5OC6Fl343>O9rw%D+A3fsyWcM4|`n zw^wsKof2xHr}GGuaU82{mFJval;pf?%oabcbF+1>mm-qV+0W=B0S4vRJ%^&L#%A%S z-jU342$PiFU{@cXn`H~;$a|AZfRs+tzAi^X`rSWW^=v2 zXg$CaHyX|apU%0OE0Q{M)B9V9NMb^Ggo>ZO_PEtI`ku*P?%JGGYX}gF^Jr|ufT)KB z7Su!djEq*p)UwZxdp(aIH&&RF32x&iI0YAM%wtuj5l;xW9f2ubg~_Ghc;QOp=rceu zKA6JbVDd*Ljzqlh()X`Y7JQ1MNMEwOC63YR`b?_ouFL$Sio{>u~aXi$k} zNfc$F)@C)PpzD>erG1MVL}`kELDAMJX$@XH@knNL(mR7o$dWBN)Yn5E+mUkqha{AQ z5oPuXL+8n!u3V<{%c%5XAEqsHMM51A5O$Hgb}qpo~<64=)<(I+V3Z305&*3X& zsh#*7K1xO5O#a|Kn)Rh49e84Pa`jsJX89Yo(3UpNO)T%JeD9r`7cGEU=HsnsYA>f` zm9xB%N*xJGc_((Bj-AqLM^$lp9U6Lm!*6ip4A_S~R-$o4Z5NB6N%6Fsetj@V3FQl^ zE!VHp*i`iJSzz=tUxFTXnB5>=h0D+djB9)l3KxBdxsF7&JBBRh?_`XS$ zshQe>UbO<_`@$z7vuyLi4~vY=$Hhibk(}(J1(+weP{H-?fYVtOLRxk=7fu(UJDuoX z;hffyn{QbGo8T0UTAqQcH5gg(L_#J*2>Bi}5psTyvGp?aV&K(wmXVC>Lm)`jj$J@L z%>D5AbAHE)=6e+5Pa92r>sn|*YkmWw2bq#0K5WBLiDISGgrl4_o-XL@kl&v~HI zRdCK5#IZi&)g&T+|95hN5Y44CSVe4c17DW8H~&upRv*rKy-*gh;hSx)&MseGXSb^! z*JwC=yPZwM<5ppO0z~+%sCz=qv-sm$;5OH!GpkL@g1ep?P*IOF<9ttAP1fRO=!Z4j zhO>riqL=j*{*JgDugdb*SJ+R!LP=YyOMbJsX|ct%!@A`pcm49fzE70#6j1=d)dhBj z;7(f-)%``bjNmrR3{@jZmV>4By`@b~^?pU4Xgn;csux7NJat_{4{qrbEWkp9Ekk7* z`|}ulYAVrEgve%Aor1n~N%5nQ)LtcDb_;g(tjkj4FUpW*SBTE9RR>dFu#^lpc=@U^ zV|DGSN*M_FhUAJ?tXHatt}ahpi>a_{71rIaO|MCr<=8wSAu)Yz!a((!PIR6VB`saK z@7dEFGf7m-5b~`fU=7*suDpB5sJD&H8Nn7;%`3ax@wyR~!nVU@A6ScOR}Z!dyn&{I zi`&MzzWJf&+v=w+@=*Ji{6DAIFS*YA^4)T+^5X~i`%5OTRWnF*fgGaC6TbqSPS{$a zpk7Aa#?pYo@*5La0f3CRnqZ77a`C8~6rTrTK4@lCfX|r5Nu&xBCMp~$>4a1`qJRus zlkE1dA~(WjK~#$s&CczIOtL7JgePLR_{|V6qIF7(iFDr1?StQ0jNED$e`m(kpun9~5hN z5w)#X`#cAYg1fWo(cXo`_p%v9{wu@eF2iZ7njm;@49pT6Za)hD*Vc$ zeUQL0h}P8L4=5?hkDh*!yHxo5g1CG6HKF6O`|Npn#WJunY~5`IB-iKC#yb;#O)t zb)@q#^qckGKg4{f4PDtT)nIOJPJtm}(;P^j@@^!SEnj><3Vhf-Um$Cz7D4WvOTC9s zO5F#m?0@(I4Fzha!W-=A;`(26r#qY44zh0ckj0!^PuQfh?3LXsB~g!r+gAqo`=9XX zw4NghokL>gXmwCy4wy4Mh+$4v2{$P#oOY9u(8dF;`zJgb+_(YnZK)Dv+}4y0gbW0b zQgAz9Wtbm(>GR1eoXQ4afhI(X&5A7B(e7w~^mve;N}jO4BR^|=Z!;@+-wSfZT+|l# z6{A5KS#G6<(=F~P-e6BLKS-5Z7B`w)&e9utp;?d515RPGtTCRUoNNI(sxkuRHNvHQ zzXZs4ByPux-G_)E)*^xW)fv&8LxgsN9fkxwn%%g|0Mt%ObnO<0yN>Ko)}a`o7^!>c zQxMj6Wg{h6uDad|wOE9;U$g}cS%9cY-)w;kWUdFnwq03w|Jf#UV zz{nh0B(-2=HdGjl3)@0R%D z{e%-;@Q^+Z$b3B}i8_6;o#C(1Jr5}B9ZrZW0neR|0=9D(cTsQtPUOwshnxZDJv@^o z&kC$3K^SORSnSnYtwJwf)V*-#ofOXo^7$_{h?}iUEY62*LU>;@F6~^!o|Tx*8h|ri zO~K<3MB%znV)R-}g4$Oh`0ieD`-?!snPnTl(W`$VZ(G*3W&`^u&ShmO-V?>wL}?Lt zup6ttVtrb&G4F04=iZ zKB%n?dwn;#BNrr>mR*HEt)0Tyx+8M#7S*kj`0lP<*3qQHfdo4e z52kd*Fw1ZB8;{Lzo!9uJ=vR_qUls_ia)qt<_|!P=I7xeC6a7vV+lmI#wdN-^+?}6r zL_{46lzJ7QD0S|_;jMFh?#*r8$#SA!*mmJ2#vElSGNeNJhEm?lQ28U>*(1&uIXsu3 zU~6-gXBSU|!u45E-$7=|MDPcOGwLgTywS5zG>|=$T*{`qQ>dGrytx-FVKA zDu-L@KlXFPZ>Kj!W6FKSPDk_`oRI}3p}fGPnC5N`YJ}gUm+EuLE$wGlLjjz&U47^( z`5Pp{jpku7gZJ(-)vbrSST!$aRP2sYO6>zej+ilkvveqiN{$GP-5#ceFd|WM1%D5#F$%*H;D>W@S{(rzIN-!%3mIdSc42l zzYO8%N8pXdbFMMS%zU&oyWV`I_~I<;a)0&Zi6NeMfgU!kZK!cuh%w0 znHX``ueKy(G3*(OQO~9v<%EAKcJYNouh9b+!rfV`Tw!$>C5>^AWF}(pi7G=(tb{s_ zt;XcGk!O^io@Sfg89~wi?BKk>&6T?gQZDPLT0n5=HYr>NK{IWZbyWnf#LG3O^MV^b zjILD~B>-zD_45>Z-;-`S%OcK_Z&SFXA;YezCo=>{?I)-ikzgo$LTMqMq$NxA1zin| zFZ6zKR3I~$n=07RQUtyu0Juy=A*8B!KqAD<;E&cQ zUT4Q}i(VY0*rqT!2uLfWshY>A5ySOIN5+ucEE1B19po$ShJmZyl~Q%Xo3To{KRqnge^1+)T5h*V%~8Y!n`9u=)O$} zG!t0RQaf8GW=dU{GS1kNGs={%L6HMv%P|@4e&2a!iik~%yAd1g^_%W8+`Uf$>;bwO zd6GLXa9JS3h9WlHuYi!QSJ-H3d7A1X(@%BC^*XyW&7y=lf4fwdgqusUsQYILMB9+C zM4&zqezz4S{Yl3SwyC*1jK~ER$k`T|VV>mp{qJ!{3(Bs#LkOqiQ1YiUkSNt;B6+Vu zQt7B}iD(+PvuGObjInwjs7{`y&W^BZu48BeIl!m_c8ymA$&3Gh{;bb^r$cygK4?!O z=1yxA(pSIt02x=4BMjc(rB{aFmhL{zlxy{n8fcP{9Y=yK-vXhX5CGbBLg12m z@#n5KcY2yGx`k5tPjVZpfdzG19G|TD#%2Z5E)-%y*Un4(^y95|c|@gf49XoA2PTn2m)%#Ybxv(e%xt8l^xC+C-3u9Xw<# zL@Umsz=OK7%R1Z2{)RhlWkgIshTjYI*{~R8*^Q(Dxo_+fI9TFF=zFED>COrKR=~b3n$eYNydH&bW|Hwl2{eJy!{qFI*#~;31t3xHtd6q%1A?k?sw_QXY z0v#A>wIhp-^y3e|&+Msgb5&eesU+Da&{-xF+LR%1c^Hik(5F^dC>+_x$XUmXOui|> z*0;-cy|WQ1u@FG84+swC-!d_sS5kIwXJtLf?kKU6y^1%V=^lUh=^?-TJT=O+_$(!2 zI;<7!&gIomZ}Msjffl}TPqT@T+q{x1JkhpFNRY*r>ooML!+VWF4j#)33_ zgyx+E@;r#l>}ogN|3#u$gLik4^oGVRr~$|YSSSoE1HG9@84Bg0>ZAvs<>B1G`vD8L zXq#|kY^!kOQ-Mpz680e- zgk>ua`-qwVc`aP2%`W|ooTV_6E;k8fF?z$LY8Ub=DzGggnMsLdf+4AhQhPI;Vf5U6 zW|h8i_5qjvtLgM#OqX5|-l95?8z>MdSoShl(d7Q1&RZj#^3Z0`xqYC>+#Z?>CNvTW zip8)EosH#;^E>iw9u0($0;ceRB%jvju94?qGwGG;Q$fAH@-LLW@uQNbSOrpV;;V?t z38NpFc{5%MRST2kso(jFONA=e1DOuG$cfX~xNQd~Szr-qaKj5nJ1c2&0!{}ohC!~1 zOJ(AViM12Y$$^WL^N>!#=0q&&0(vi#yIqkTI?WE9Zw>%z?wE>iCjDj#RUly({z>bI zrgkSaGQ5~}I7Vjl5}mthg0DWCL22?Fm;+3cCcf3J0J z1DiQL!LCQRT?tc<5{}YLl8aMH5=jz;(?gO1cV(c}64CnrYLi1UFvVRC7_0n-3Z%mS%A9loIL%e6WiA-v}W`_=l{?WbqZ;$^{P+Y4Y8I$s%y7LQ4lw=T{K6Ner?J z)5|WSO_H}*#;2379^f<_f0tsH4>8wMQ?f%^`OSKYooOW4#TC!2m@RSMK4xik8B1RaV=;^(N!FmZn^W45Ut z{Gb~uP2BolX>bW@4M~tc=3C$&!2qfu2g)xQJsSR1UP=Xt=H$7l-t zCuXT61}YZF{7R;3Nyd{xB#Np6LoUot?nuAl|FEw+A2*%)s;gmCK+nTw5_NkO_4>-c zP}*l-!Vtn7tF~nl^s?g_HxitJTPDOu>GYc{jWe=iI!fRY4QpMM7GT7~X55uCtT3|0XatFE*SVPHAsO4kMK zDoXl5-j30hO|?EUQaiVW8bT5+3tu;Hovr2(w3_Jnyd9d5n1DGn+o5sA4eqM7^0*z; z6Wc+Z+KQ;zG-hlIB!UpW%_0f6)6&y<**0S$Xx-JmMz|o|TwQx?sM=8H8dp&?m8?Kx zBm$H-FP?u118&3=t}OYkDq*$aED@7M?;|tumV@5#mmq7X2x_N?vBgPab>UZr?4#rJ zjcw18xtY63FVbi+ys2>5oY*`0Rpr{~T3HDP%uP_g2V|{&LDoJ4K|Caerl3!VPp$Y6 zBp=AS4S}p%31rQmSsZ2+c)oTg^fswg#i55 zfNcVo37v(bJVKn$sl=$G<1Ms_Ffz9RH$xXAQ{ll$eHTXffX7DjJ%HRL=RRr1FvE+F z!n%1^`(~P>7wHYtT2$&2-ES(qvBWi(h%T=KVx+Bk!+G*A)FS<=6~$NA}RVX2dlxNSf>Rqpy7dSq$&>L9*wg>@T_Q z_(B!J2(UE7C4vhHwx<^_i245aX2UniaygC8srq2_2c zD%e7lnwWiT0@SntV8(JU;#vIuUv|bWoa}_&rtybRq`|QSWjWOaPBF^OmigK38 zJ+Af$r629Mu44&KjP~}9#WonYk4Ls#tgtaCjvs}w2;$l3=m#O$7YysWuY9pnPC}2l z1`ZRtr8*`oII@|8A`~VN=EAt!YPjkG69}H%bYkkbFVcVAg9Zt}k`~a1k|z$@Q0g0& zB$QA!_9D>q5X)|gh#B+EdTe|}x350>VLyp1$|4H!zDh@wC7G%M>|%fIY}rmx^pd{- zlPw5EXw}Z_zWSkehS$PRSM`&8u>}LDNsi_Za7^T#*{qcMQvnEz&JHo`)E1UW2~z}; zxl~4>A3+Jhnc7926Mdl z>{onl^xK1(zL-N|hCdzfu)+$0pAis3LV2nzj3>CIX(;R@3jNK%0>}h~FN2#1sG-!V zHDT1?V9xU%d}JkYvz*dtOp&4Nda8oDOTvktE+{-;WI>Y~usOc?z+YVlijY+1fO4e# znLCRByWDSqdW7YMW=8=zKhvzx34HDL9-Q2@ya3D@92Z~_J=M>(pNKovTFGS*CIMWX zEDT34ociX9OWreMUK2(?Y1(odlaZA80$uV8CARdbovLz+O`y%>P;_F9E6l9yJj2)k zo@&2`nxK4-H$Ry58QL4ruYmW@ArdD>D3&f05D}c4_83RgF404hRJe#`$q#_mz6^Y3 ztta0QB4n>Nl|Nj=o-opziX~1c&VtQQ)9(zK(JRzjPm<+mol86c$VPG}UGRGR!{2_$ zR`sHFenrt&Fo>n!2uReW5|9B10==qbbyo?jC#e(Cp(%4EQxxe0TwWuBqa_`tHOz?! z6W+%G2a}v5(z)E{A(XsNr|AAUY!zWF?ls;k%9NwqrpMFAlz;#5kIYR~U)^to^fWnQgmcJ9c8rLk@-b3P`ME`e_9j%$i0xfz z?-=aV%#Cu9u`_3H@7}!)jjzRti1G1eQDOD}r1ibp^&ST{M6hWVaS8#Eb!Jo7$n&t7L@RHKdVS^62eCKkjF~@ND~HJ1 zxxJVsCPF>Xlq)$ieiQ$s38?qV~%`}B(#xBiX#Ar*NixxAAO>XCHV zN{@aV_z^Ama29+VBw|r|W3k^<5bBMLUGGA9bz!>}A-ehw5khwPv%S>CM(R&?OW$vm zF7`D}S-_#l z;~)P7fAIPVd)D<+eSdVrNc5ya^Q2PbjT#Qq-}MO`OZF&s7AZ$P|3Bt}Rexetu;?&Mb}G9NPkK%EBSrR8uCBYL zIFDi^Tg1?Ur<6FWH;0KkNKgd+>GzqaWAC0Yf1NYI(^IqO*85L?&QQ;(WzpYq2~fsO zPmMpTOHgXTr$&Ze>(d~5kk949NXA6GXjBb;s|P}krGdLc*@3CStBNTO6^Q__-Zc=bD0Z?p? z4IyEE_i=(B%~w-(2<$!#^BhJm! z_M7+FUfFI|K;o8>ZmZK~!eMiwdGR-&PJqHIdolRd zh=_|x_6>nIL(YaQ|7HM2!slGq{ubLLW+)KP75qKLih&6+F`T;MH8Jvj@(XzcHPaEK zhUNKG;}5JC=SNS!Ykt(z5tRsJjn!#=c6}0 z&3>iJv-0O%Be%}3&)^YWihdr#y3=!teQ3gLR~m{nPGQ1(ssS0q6?N8mzdrk$k{+Ky zca`ki&S=$f(DR(5FJ{9TBDgcJdn zYUlfwRucWe3D5j$24qvFC6*KCBZkIq6I4_$bDik|9U7})sK zqRQfbpu&KghiLcsMf=egS;iwfU6Xm4gI-IGep5%~<$_BpVG~Uhx11ME*LQ@fT~wb-{~c9{>8?W1CwE=-#zee^4Dw88o%u zI(G3KMGoOlj5rg1J-XH9=vLN@+j7act)^hzq+b-1xviVb1<>1m8APSOm~!S-TuP&h zH>hJpl1uN#O~ZucTleW_UrxnJsZjFC6xK_b5WDx_zyI>9;Z>YT>|vjbhqn z%avo>RrB3?jFIFmfmUNG#a4GOe2A#+>R_x^6SZt#d{p@%(F>_LKz5tB2+HaJAgduV z3WXG)x+KLkOGQj}@2s{~K6`I9da^9J@Rs|r+F~(MPu^mm7csd9lC2;!R38Hzf-RFr zv~VJl&e{>7w_G!Rl}G{P9dseEh8qy-vhwaZsq*dS3C>qBbr9+ zheNO{h03W-?Nl#!7BMOI5DeB3rMAp<3zvZwhLkmPpb@eNo04j3DYq>eXN5>9`Rtq9 zH6_U9Utl($vfp$cRhvQ$lC%Ozax5rIk~_+ZBzN`8)FMXgz%9D25@Es?euQBM5Lk&K zoLs<>FxyRC2V;3PSNIE)WBCzswKg1jAg_h#9w?`uOSHWD>Kc(A!Z&DP zAVwNohPL|$8S=%etN1fW?aMNFd$i8WvMY?&P(`SRC-{pBX%JH=rlLZhv_j7k=nrPe zRzet>!;{Q#fP~5FL>Lk)fB~_9+KIi%$;qpT-&jDMJdat#PA^Mt#fI>#w|OxkvrvTG z^$WuE(Lj+ah{RMU7iB;M;2SO-p`^Y3{isYb39fl^%6f#hSk2JuR0rzf3mHI3)39EO z(BAAtg33B(7I_Ry$#5;qUBy^^g2Vr|l#v-NGt+yKlioEay&r?0-jC|11S%i8@Rlhx z5nmcV#uuY|qh9(by!4@7`iNfoFfXlTG_4j*S@hK=V)^bG)L9YMccF??1)nYH_vAKW z3K8pf6tD7FMa{2Cei{w=l$ z%+S2TUw?+bFPWbArMkuBVu~Ch;2mOy9YR#Jl*(zeDpP#5I!Epl>$VFGX4a!m#Z`Q^ z9_`KtLTtJ3u&q4IT02lQT^x9GEL$nqbs>}!+he-QX~1^nn1x8aR$3OWv0}Jd#c-`= zXDFui)#`{VZl9&L2jrEX+<%1zD}D-ud|HQ;LoBXmS#$bhhaLI>1lhL&%N?Cd)vn@c zqHKnK4DzrOB&+*7oG|w@Gr~x9-T5i?uDA*JvGcd9$ZMIj>13X%tEj^7P!|xqnEAq6 zP%iE=PA8ilIzAcZUT_A;A1}d4-btT@u#aBU@^lriNp{s6s0+myd$ayt_i`l79T@%H z3uxtpUs(O4XD`Al2{jWnW3ruxvya_jYL3r~toP*ZJ&(g4;VO}VA3W3$4Hb|7`^Vp9 z1gbv%gr_m#?gt3~Tmzv>Bf8?)bEhYsyE+`YW+)ww$Ic!4>r`{?PQX>%ot=j6P#3DX zbQXusRjCfZIQ_I^zH>v<4it&12PNuM2Pgs4rSb^FdB`Rpa8|-)bkVTn^(#5;N~T*$ zH7j*>Pb%F9lywteS8EZGE9$cJ&0=cQbHZI{2|8^DI#mjgrCcT0wjRPEwZ>VKYqaSm zLLn8#*^=QJ?YN2!SM|C-OrH_;iCJ*5e;r&z|E6oaUHw++bPxb>_zg22`RG09=>bT- z$ihvd>m3#2;sev?-KoVD(8~))3wfj8iXUtTZ)9NUl_g;qi&-_QCTk{Y#)%*D1qJq< zwb-(tp3}_=C_CCniMjVPY-_}a4HFUGB#fZZo~!FwFb@6)IY*ix%B}rhPK{k zXOm)&D#Ns7hbM1zY}@H2ek|(7VuneTtqBpdNH|h!Om{1IcKZxzDr4S^-9<7Pp%=Vf z13=XM=^Ac-9^Dn&F>6{rvE+T*=aFvS$f5C<>vW#p-uyWDOU^ zg|_v849U7oRkjg+QLfrGs@*oSRa-LC9jaY-p(C@N8l6K2qBL;0Z7^fPYsJhZJyHSi zsFwv1B;7^dXbDW2ge$8ad%vPO7kj^2zmKsuEczPcw1ww1{vB6F%qh{PZLKtb!*f$E z7;=!xf0zfJaEjfTdaC=Tu>Gl?%1C@_x;{b(mCleEx0I$z>_*_k!KLBB5D?MVnY*baiWbu!*=dw$Ne=^L` zbBu+|ZiB0?5W7$_4xXx)%H2<_T%V@<00J;j}YPK0feKp&%PvDC1Ae~2%aG`K+-r!0`J)))tuD6eNNLmZ@X~3PXWq8xDgt;2y zVhjKm_S1OQ7t~Kj*1h<%$!)D^00hQhyo!siAMs^pj=OuFhs81Mb5 z=G)KA6f@D(u$=3m7{4r#6YpYj55+woMw?m9mAhbwiFZ8fE)tA8BX~-TM%H;p=_4Bov z+|V3|HIKuJn(w;JDR^Yh+2a%oqh0Tr;nusZ1cYXdr@j!=uyFNfz5vyJxKG#(F@fMQ zCWXdPI8N2pX-e?cL(=@i~(AvC=teWj9YAPZ?w|EPjw*fgG9_h8$;BO8hLZ6 zR6;2J4uSs^37tJLY-NBsabYR#U(EI;+Pwsu7qg?Z!fb%3vrn$LlV0t+>MnYeZSwC{ z?KItgtB=o1qZ=k>FX}--(CvO1;Fb~Z17gI=sVEc7uxF-FDhO*HC#V(` zQ88&$OuOr20aTCLu}1(rd46gH-c&ejPVAlhs&Z{~t*m*@&&6l_Tf}o!#==}#EX+R4 z@fJ41;!Jr=taoYvNcI-(UPA_87#~Kvi4yC>Ni&4Wkxb%x1E4IA{_XcaXX3cYdc)uTkYRl&N+uChak)wF zzR+pct4g~*YOU){XoYi|J;`*71Q3l+51FF8~v~sc_hw*gN@E z<=W_4S#xuE?;4X1o0n;^-r?T8jxYJut*BS**0!BVVk8MRk^EPb=2driuo zl~BNqyM>wU?B4I^P$2fRB!wsmly(XT+;Rqz+j&ROtkbHF6$(E74MN!ZFC>!ltDp1F z|7NPIRvE$fx3EbRAzLpKFWh|&nTRR!Jy%iA7G~I@bPV|ypk-e`YA!||VrS4I5@c@T zK`a{ict2yGtR+e(cb#6{7^Cl_PfzBzyZwwpu{RXR$xWubb#7;42B#vl)5&>cTIb4# z0kFP{2OWBUB!Hn9ja8xP&*{_BfD*6#+^&T`h!JyFoSuUx_RrH*Iv?r!g1_(hi$yS9 zt(s15E~ZB%`|wL2BJ~D4GGc@h#Sx0Y2MY=D7TyU~e2tWR4ZsH-3I1Aj1N9$Q{P89~ zq>cX@H0I+{w>sh6)8SgWXCVXYLp?yBY8_5%R6L=S=3*h(C#xG+FqAiYu|-1|d*9w= zNhK*31V&3*mkZtQ>VEV0hyfx?wpkR)g2>9ENOZGgL?jg?8z8bFcE7#G)(9}W^BUSC zfa1wlRz)t7^@o5kw_dx!J9UjNUB#hm<@Zn`j=+^Iz^*UC1uJ)>vx888;9PxqbDDDK zo*+vEVYCtyf-SH3A`^}Z#=-RFn|vnZYt65ZLzVNJ3|EGn341pmPk5;(5q17`s8YFq zNGJ|bidmBh@xXnDDt4u;Tl7Z_qVEq4cO%xpjaUa7u@aksTyDY0cOHOLCZHmq$CMXTwK9S*2oRrePRhxBWs^T~hzeP(RA14J92 z0+Ks6vus&?IHrEAp3jpk#btXQe#nEv;RJt>*H86v>u^&FPP!wOUP1<~t_x=1#N4o* zxuOJgo)X(BvNK8q*D{`Pb&aRBjR6u?CU6@FI5gL?cN^;5&aM9-T+*b$Si5FSpnVj2 zT*FCMxr?OL6(L9`myQIyujOo42&QD{+HkX@lQY<;W87Ah78M# zy_KUNGIE)k845~9tT_op=n=pJR`Q0h*<`RyCSWju_9=XVUKo@n*wvM#-%s@^ceC*m z3hLkg@&|}5x-`=T5pT`Jt)c*qKykn8fcJpK5-b!KpQbJ@+W;F*Q#xx@=T?RfHh@?h ztx!rc2NH7#BQX|E-uMm@Xm(($Kj{ZKmZQDC>kS#FA^N0G=_xSHsLiVy*t6` zxM;huVmsjUsi|~tD)@mpD=B=GKqxNTH$-e`@5d2iF{ zQ%i+6JDDl0WL5((@0tTd3kvA~l`a?{nC`f{EG)hdcZ~`e!)Oi5;+4>5d@XOaP)_Vd z6s$$YN4pTN31L#*y_PowD5vfS;t4RJqSL?xr}YYfM?{NmMFTihpOplq)oCU|stKt< z*mGy`KC*-ryBG&wGiNqkx3(+oK~8M}R9J$P1oa)bCu>`ajSY<5N<1-K$^TnEiF<^R zTGfO`#!G_ zR&fJyk#jx_3mc^G(qNKYhC~nRdNgfJ)>cxqbrY9_!bg~G=YKM=Q4>RN)?XBWP_k94 zVQ;0^MCTl=yVTg4{38LXdRiaOcJ`>NEKWPv_7MUijPoE;VUK&}>aV%K@}r6?8f=Ez z-PV_hce~l!lPH2rX?9#tfT6K6TmqgB0pyo5y;YQujS3 z%b2?0*|~0K{RDuIG{CFhn59&(;a3@*HS?{y$}@N2MB+UPj6fYHi+I}=%NUs;#a;(7 zmQ=F~B5We!8WcqDa~Fp`0%xo_rWj5jXF)!Q$_KdBC3^z~q8+Nbu|%da7$n z?nJ2zT#=v-2}Q2gbzjjCV;)BQEk=oO7Ae52^KPY{iC_SgVxwQ@`xULoC1b8S>hOY0nH;1QKP2gUOt-I%tS;t;1%Kc+Gjr=&PluwC&6J*e zM3fzqp^iYG{z_R`l!|4E={x7viF!W+zNDJs#<7kCK*>Ot^{&nOkriRFi)1 zyeE+f^0w4*Xrg3vWOIY<+&wr+ha$(lZC>UP98wC(|71|0TOKFf(8-z`#ZeMjpf||U`KKjQ(lFLb9G>{{b?JH+IrHEhyD$R5E` zA18(fn;0IT()j!1zw%H1%c+K3&Mw4S00~lA`th&LUo!(| z{NRxb&yWAe!UQ*h#Ij?Y){;iCH>aVxyQI^RY-%pTI0IaBWC`XH=wRf(314yXcl3LR$GA;xlx=D(NKPHA zGFE(?s0K$A<4WXM6=BvOcW$%P7<2=TsupOJ#&9g!!O^#Pqc(POCO*!@e zAo+{DQyl_)C#Evzh^vC=O>*GBC`>^6^{Jw8 zo^lhF6X}G-W&PLdajA;p{O# z3c?o^C=06mrl8-PX!M&JSNj|MwTauYv8oB?cg#<<-)x&9Z{Pw+BCDy#xQUq=3NXsP zm=Ymg2^;xF-N*;hsc7SvviCX9;IxzgH#23_#elZ)jICB>!sI2_C%V6+#~fq`5E#8yRC8Nd*>SaqNXp z9)VP(eCM|dk6p3>2FOM80hHKe>RZN6c9D+VT3#fG6n}H8TG#Ug#2(CI*c%Fjeskp> z|E+S}+negtA8(u63W`WC=9OuGKixUOq&Pw_*h!Sw^2vqG zMzSq;mo_WqY>D$EN{I`mQJj$TO2GEb~7V>>zdD^SXp*Ou#aTn)TSbeI~Tuwbt>KxqwkAW4GDIe zMJ`ByI#&<@=l&hfsv)*HA3$!AafFkY=H>v$VgC4k>Sr7$BDQFQ)-btizW~UWa6}k4 z9IJRy*%EQ$93Tmd*t{}1Z%#Na%*pa{Dn>ENxgZOr-XY9&vcRG#NptlUqZkPOB#SQS zgnYDRMk>FZamet1#W(X(9h)K$!Z|mXDZYx;n~?S0cTMq~gq>j-_Sdd1hH6alX9}zo zA>|xK9I6Ro#3P7&3$fhoVO5S@T~b-3mgEc6-6k2K`1@ju3=_`JZZFKpYx84RnMd^M zl8fyTQor@f52M%c0AR0=&vzabR{Y91c$c{%vk5JZ@5N<`!i4vd0rtDg6p<{y_g}kj z%P&7JQbcvf{y5fC?iP=8)ltV7B(E|63GICW2$4sGOyKqCVXViQq! z{gv?I-mJ@Lp4PBk8$YO;%Gu<}W|F6%7XhK!q6vLJcjl+f${91nI%E~3$#mrrte;Z@ z*`?g|lZiiL)z!O+Kjq9?JT^gscR=2Us(!MXeoCjcUA7`%N~)=+rt?+*l)FRiQsg_~ z^rJA2oOE#(#Jh}_e8x-R9Jpa0ws_FtoR6JdBp`6pQDn${R+tHs%l zKQFR+l*Lycd;3cqc_P6sXEDr36QLHryyZdszgVK;5U&fQ zzNydnB4^C81t5YGiA3uMB9m-)3lnasaWm2aed)tXIM?(^>Ikjj=W|k@nMXVcqQtesNXJ*p9ndlT!=F+vnyY@osU@V=W4vY;;9D8Tqs$O8C z>^AM&eoakm7ldQm=3Kx349}ygk&(}VVkhQhs&(@!AXmf8OE@V@m|KpA+^bxYYI@G{ z6%`eaYkmm}yz7B9d50)gk{lQ4+z^Tbv7UDrVmYrQ7x>gnx z497`3X7t0dAzxiPzQ!hcwHEqoqN?L-!nx>c-TB4X%`NACWsElFPJ8~JyW5}_c`A0d zOuD%RCEMV~Py&TeS1v)@v21PyRmX4#hXIQU&{FLB0|+TFQ!yo$!7ggB;q;uQJUa`+Bq7qoIOH!!r5 zbMkwFC+`RHc%kVL2p?xdROJc;Xn(yU%-S@I#q${bn0r#w=CRy(hq7IFhfsSIfSfWz zxZvJp<_wZNpVIu4O#dp8y3uQ@CQvy#;Z~BEn#5T8&HlC?gq$mvqC_mNL|k!qzLmdS zlgd^fvK+Ftf+k5ss+ze$sn?&u*5Hx?joF%ni&N*ku2Ds?}f9~48$BYK@ z;&ms)eci+_Wy?Lvl*81dUM_{mws|wuC_QNLN&%UFnG>D$h(l$}4F`&3Zq1Zv6NQqpOdBpw^KIwbC6eAt;FhU~lT);{01~3w<=JqHMw5 zCU%`*oK3EQ>2WXpeLJ#yMcZL0gF-o zJtunh{8-_M>hI(#83Vm z^>iKr?Ir_qVi5<#aQ;K!uJ2Ytq84!y!RqUdFED=m392N`?>-l%>X?fTgjqb3a*+)9 zpsQq4*mVx5v#uH;C@S_PGS0DswQ;&fJxgsclgpCkqjY?(XG@I?C(Cbyf{VG1^4-Ub zbGDE9JRR}W$b{=W7}-g0beb7i6nZ^UUm05%04#Bfx+7tNOCk4E2wIPTaCGnWsQ!{c zO5){JlyOUTKjchCB4~D*(n&%SRmTE$*2B<4yOoIaGnW0AK$+JO2D_{^AmF1Rs*y%K zVz6qUp*_i)*}@GTioU^PLTtYJQ1~7h!Vo66$~vVw_vG!?izz+T zE^|Gt^KlEXP0uyxAUE<1n$~TI3YoQ8e8(W2J5ZeNzKFTCR3DFw6@jxG2$18s3YeZ6 z@9EM{&&>$R0fyOepMnw%4SLSN)LF@1b*4WGu*Z!ljKKCZ0SlT;Q|M71$-Gk-je2=F&Pb%TM5JpO5_HZSI z%RZr!1rSEIDa7Uw(#Pz~qu8oll8@?8a>i$&vlp@3TJ%i_x^YHvTT8}p@gzPDn+m;3uPh0} zxMo~4PW%u%HYbqMNddjQaI}%;EmEM#l^)$Ct>T%fyYCicE+jv_o zMR+BFzWjjYIx1uzSKG71kgd-mw-{N96U}hO?b#&rV|+GRL#7qZs;tB&MiQ#4rl3DKce^}QE^L%Fxnx`sjEFwDCIU@ zo;Mi~uOvr%wxP7g2H_cy{+emtz2^bw5qyCS4afnx$qR&ZV2WjTYacSCqW8I78?1?2+M+8=i)IB*g){}!sZnGol3Ex=}v zR-9L4^Y!xuftGi#pnKAx%QX)!ZZ^4gZN?1$^ZoA?&;F%e?K8Q_Fcd9@GL*8FMZ0$u zQRq<;;riA{gjixm3tGlY@DS)7qo+6GJ;SxkcG|k8OK|Pg{8YPX(aluJp`1=lIe|;` zbw&PMVT-=UZ{%gSw*P-#6}^V=zI3n8b;&Ix}I{wI^ys7ytyCicF zFqAi-2*t5Dlxwy^`S*TUK@8`%t+*iR5ig{3w`qq9>ZZ-#odHO!x!R`qDQXa%WM?}F zI%M2J=HITCg|@4^3x|*y%(dZ9P$R--QPLX^V%VI)1bZw48Hr~xE-E2GUIWn@Vn#09 zz<_SPPQs{!mOo^oH+JWXJV=t_NyzNZW3a!RT9WM88lptcogdUgVd;%Up=x`J=Tu38 z)Au4(PK5I;3VLU#c4BXGa`GzTHxXt`6p*ly0!h7q`x#J5L#5J-O0`MnWb(ztepT5F zY$j<~vfMjSLY1y6SRA3OJ}ZilaGW~``NSngk{40$MY9;UH$>z;Aw==PXWsFfFamYw zF~6n4Q~ez-oP9HoskmM7%IP}~32**k#trEl>wLpjw}q9hTGfWdp^PcFwD0uj8hIWz zlU^lSTkF>&@Gl%~l?*{wAln;1DtU@SG9qH}RYc{4(GSeL8Lx$^g-P<%?2sJHXtD>^?s_fqE|^=eff^AO4ezJe4?-en(NfC$*(+-& zoK(=f1<3WF9zFA`8HgVQYjq9xkq|X|?|f_yq}^uB_xITjf+2G4T1LL%d7mw<=g=g{ zmQ5Mgx;GO&fRwm2v~Y24NMuJ!=qD@7v+XR(q8}1v4n+jH8hha)B=7La{qXnpKO3EE@=S z5Xu{;t^xJ1#MEI%>NmmqaV|vxz@~CKHr z6ZKVMf)y08pp*85Ei|^F4`gi;W;v7pXNd;9nTK|V3lKX1eM5nkHGlqV25oUR8VG7k zvNXN{P+`4H_SpBtB5&T+jQLh*S1oZWof-Ybd6tzQ`o`DgnTz8 zzdMPrdnZAfQ16y%{gG4#P>N^H4C)ksWmV*YO?Zr*QN9B*!QD}zCtVONB?H{-%Blv??XRyyX} z8gv>F7E|k)etA{|#F&GDz$H$Q(xFKcsfT!ACg;OQ&?+d2ZWWY>q=FPcM3yl#w4HT; zIIppU`@IF^ugKyzF7_;0xd+0z^fq{he3`rHnVk8y&3ol!JQVytY;2o(xIg#MJX22K zm&go0$i!$0A*7p|NoH62D(=B%!oTHMK)EP#eK?VYP`(VhXb|cfnID_`kB)w+nkC_l zB&=@n$pnlqY0>CUN%q>aIqqR6-~e z*1vFL{h*!y8FWz;rI{&q=@yURF~OmMbMzN?Adrb*TXNn`cizs`t&e-eYWeNHs!>vG zyp>grP-@Fm=zkDO4cy0X^|fZ=A=EOX=f}i+T6s68-vdF^_)Su+trP z_j_9G5W;6?$BKP$;cczz6J@-Zbgo8Afl?Mlx$ z+9pC>`9{B#&Lq5%)Nk@+A}3mouxdK6zQ`=+W>4C209y9}fc?xJevxJOMO?#DO50o@ zOv5tzY00bSZMftR2ZLx}Lfs`lTh!^kAyKeLiZgxn!ooH`NMuZ~@z zC7u_b?~3B$Q}WvQoCQtd;3DW6w*4RxQaDsTOXZT=`bi-BU6??yzIYa0rw9q}q2Qa~ zR_xsgu_q6#*=$7U5(2S{jQ3Fs%6qX3ml*TS z`{?5IowwG`OEaXe(%+hwOE_Ub$1Cv36Gn^A2jTvW3={pT=Y1YcvhOjAd4#u=C0a<1 zFNVHEGbL%9hL1B(UuG~4#e9TNYJ3Jw@`VxRTm}}Z_)L(dWBQ9LhG(Md#tT`ToxFyr3(eh(4i@vtWnp}n~jg%p_O`Z4)Z2%OJo zfcN3h%B7Na&J%?=>SsZUR<$=y%L0X!FZ z1sHKW%QyQl_BxO-Yqx0ac8rk&go_+FqhF*VQ7#$xjU0bOxs@j#Ltqh(B9N#8B{zC% zyYHWEp?3yd9k_HL7f(N3v+7_Ij-=wlP8C9M*7<2#kIOovELG`R6uVUm-5QJBY6Wg} zyDk8P?`5`ewOtn=yGcbvmRDEXL4>mI*~v(JNe7kH5k03oZ%;mXGh#1p{UO~&q>*Ck zu9vyFPA2Y1Dc+{|*1PTcnqj-jtlL$xNZ2z zoWipDW3u`{u)T8*Od(+qn>mR-5Q|oCuPAgbbMBSxKLg-gS@`e-ie->|&wH<&eK-RE zbDGZ-2&h=XTlwgiE4YtRpNXeA7(|qL*wLjqAB9RfSK$cJC$=$dw&78t(l#QkV`y9o z!DP}qYf%em2xAMO?mRUF9}Qs$&s5==!8*IiMKGjn zn{-DW05x{?r5_F|9;4etvEvAWwZvSUNIh|f-W!F>T?7HDR)7bqgH7CwqaUy_n zH=1%Ba6&$S#E1_TN(etiF@1Wc4y+|-``mfzV5Lrv6V5jS^R7< zHntEKo7@YPf4ZfmCt)Hl)Ng&s9O&WVK!KVi=(2pd1BhqR#rcY2a1 z$y5LDyT{ifESSNs8(AQfRs}#9wlN_UZia`jmc;}G12zbRmVyY3I~3!Gwg2u`es`mJ zW)ORN!1dwz`F>=r4q)uv>CuFj((}WYeVh~(MmFpKrgJM2N-MrcfsLnu4N z_Cok?{fReX%thP|L(juz6776Z)axt%Lg^_quFR<>*%w>XcghBOc=jTb#a`I8FCe6X9ePP zdB;lGS%*B^c!e!n{sMYsyeqWE!2v#2pe|$5w z)k`%tQ}LP@ecx3+ZZniOlZ{4~I4!c+tO$r)V}MvVHJzPHeAnKhH{4~pu9oTUCR)$( z$HWvdrzAz4Ko@sHi7owD7qKCfm>A|wE=gYMG<|XphNszKCA`y0iq(_T0FlvCBULSE z>^W_=xgRz7_=E34IP=$fvNMLu``l_DzyRf3gf91h=Nj1Pvor%7`dED5Cj)3A7wP{j zVdh}bN@r=WKCrEydI7|yx;hL{1lyb=F+O%Xy%S4C9IOzsxnIG-CKr${+?$1vJ#{N9 zpPSS^SxlkbAS~alN6R<2wV3n(<|YPt@*_(@ohD(fA%=$M_TaeblBzwdur7s1oIiB0 z>5^Pm2!I%{{#^zx8zsN=yPztCG^gX`?uu`ICsFN`E^Gu%p&TKtC=iuaiN8iu3bzzP zO&KTn7#$c==r$+t$6n*z(zB}KYATUzCJ{LWFrPP)dR!wlkp`dRZ zkVI$fLXS8(*;yF!19G4!sT1I}2{NPE1P}JcK#dllF-8aVQgvIXGLjw{(XLRGllRjo z#d50X;x!h?PcKDodePkU67$Q8<(EA0IEI8r!ZKcli@(2u7>iLreB_$JKano8M{U`o zj@hGr(m*v`XT6cBp>1TDr!Bz1t*0{gtS8GlB2!h2&}XC7YmO%<(NJ83PK4UJ2M^m_ z=qURHTFY#(S~gf?Hqh<$%cLVB0-bM*5CHx5DY0b9pRn7|n)oI|V}dD)-e@zg|YU&s*3+6x)NqEt)f<9R|i$ z@iU>WL7(~)8v$MnMC=uVB9IcZSneKNluEqH?JAIbLw-^_15)XWOiiKI$ zv;j-~2C`I8kJp^b;gHvBkw#3Btpr>;5n+)_nEh5yxz&?wi@{B>whx~~v*XP^W(o_LRU5y>&NVY)%QxS_ zChV&*BGfLuhxIE%u_ml?UELxV$l5Z*o8}2F?i_rIzTlOJ%Q{k{Vv3K&T%C*t8vf@Kf7h}sj8Sf20 zAd+h;C>f#3ER;}R6-zHF)h5BN1CcK#!rlXxHe@Bdz7*~52Z#HN1Ca^vDq9eucH#mzqi7zn2*1E!cF~oTC)^!zPe*Z zyqTQhAw*iu=4{Cj*uz+hbyLe{3y#VQ)D**=Vv0=W;j> zD_*9^NdnG#BG|5NT-!JfAcnsMIed$Rd^=UYl`a#-UnB5eBf($q=k6}vg`@8oISZH( z8=4nurZW&4E?JFXVv4Or5UeGJzW+5%P3~2?X~Vth>f!=(q0mMKW+4+=8!%3E>7ydB ze^*Sdi(1;fqI+I)1G|e3tzS`=Q_OocmxCpt*jEU=uaI!QLY!PUkr0pI`PagJ=GXzK z_C7I@N)v7%{WmOM=EDr*>IJt$jv;pSB0mur_exM$=I*wReDvmr7 zLH<63V~Ys&E2r>AkTr?OxEMW^-4tfXnUf?ol~-F!+3(8q?usTMNP#T!cP!)|zDbibso3fL;^|a9t1xkYiEMO`^NIYHB|f*O)6|{w^;z zXNLVPvUIU_xC_*`wnjKBnepwdXQjb)oV2(^xh!&$=3jeEM!7F|xzs@Puxx5pbWI+( z__7iMa@qAZQsRf8PgxOpo=Cq+XDl;y{UW;4sNLnnGUgc9SL$AM3>wEqeU(l0L4|f(OHh* zTzv-AYu38W$j%E&_%+BMMkUJ=oQM@B$C3iXscdwv@+EqgZaJ*oRP24_Z&B zoTvYhpDS*~k)^C1$A73@0!6iNJw6wU4nk3E5%5ki!_KWE{?s>rDo2Q5D^YB#f<#_Y z%$-AKd?FEJd5^GoV;bGk8bP#TKU<=N`)B<2te#l zk?1;KM0kbb!sy_D2a z@Pt$`6(?D4d=Ai(x?L@FSJywxDE9W4(9ih!n{TZMh5+bL9z}1R4CB9k_se%*tM7jK zA^*~yKlr4oFpI|qt`-OB84iz=Fo}Rha#{Wbe?jiLYB(hzx=PdmWq!q<27l3s6>kCl zTAgbA>FXmBzhrr$--;e}PtTk?NZ7%-Q`4*Er)CCb-LstE{qX1fJj;z;D281T#K=|{ zM7T);CBN&Oh!H|tr_utrVWkh3q-Hwd4_+8H_bXbtf^VCug$QWox$e3K1K3sQWOu5^ zdZHX*1$$acxIms74j3ko;H5uf5RGx6!Am73ZT9udM>y}6h(BUo9&Z64a@gGf@EBx;HQCa#RHkr zx*Mx0M-GN%%uXG1oROLQZh;8IyQTBnVpq#A)c}9Tw0;$J*#FGNn6T#aD_k`UCi!=s zDXV1pob$MOd^oSwbV8rQt&ZRSU--0HFty;Bt2vRdh_LGWEI>MwAO4LNEfplhux5ks z+;eo1b)(#2nMF7ru{0`S&kVt}nrfAH5@)$g6M(N|0q^c5R)SxvoGJM94fyDXpRzZm z3iH7P`moVCPFa0D~S?b(Q_=9uW92nw(e)}VMS%3o$uPSyI3 zoEx&OrZ}FOG;uWnw}yi+8Z1@3i!9>=@jg)_f`T<6`R&IQ$SGB9@mzg{GQnOXWP=M& zY3*Q?+p-<#Rw|llPQm(gZ+*JAK2uQ0!Mjj!<{ZYHGqP}m2%V2>XSMMhA~KtK#vC}O zG##IF>77H2E;*ea;90A(N&paVaKGDtc{WQ}!(%Kj5|N9KNQ7=c!I;~_ublbUQ~fp9 z{t%G${H!%bez5?R>q)rIF2i4+bL~J))-Qr`5&PW_`S0iB)QqsL`|I($Uw)teXY%)d z$bZ?u?{l854_&QwQGLvTPVihNtPxm%#HdeHBSEpQB^Q6q64aO!7r@WJ)r))t{$J-D z0d<#Ockq<^>Oow4et=2jY?|o9Ve;&jF|UYs62vdO0mp)@nS}U@=H-6+WSdlfP)7Y4{PO+yNm+L4eL1NLnd0ZR~xy-6#HFy6+huFkbM z7IzEkjJ}$4Zxxy`lKO_FTApM*NWBUxOGzlG?O22+BMUYCE*h1yNtmHj%UhN?mY14- z^O!fZcfkE>hI5$shSaaRF$kpfoO`mE9iXZEHeQT<-ec5 z`xwgM z4OHzxz~Bc`u)*Z}njaH>54B~3$p-i7=E%k8j-BJ?_`Jc}=WU*IPZHlH{6XWG9UD&8 z2Gg`TE=^C586Sx{?r7N^utJ7dGF=8MI*@ZvbJFhY*9Jy0`3E|y3rczs6@Y;rshS*_ z4t&Wr!(-Qs?(K8q&9?DnU!9@ee$t{2B-KP9O*tZScfL3 z8ful$%ekUNX2G9h$`$xOfnI+FBL8Ojia*--2G9&=@#VScK&d~ta2_`Lg#?9>TmG5q zcD7JwoS?8b&ucAr{QmNnzrbJqX)e{K-fbJqr!6=I0c|bQ{F0r-Len`NG>D`V*mSu7 zBrQ>0gFpCXS3Tu^ITn&EW9akq8S2-WNwPazcZ4E9qKudZ!86pO{YYO-Q=PN!KM823 zp89Uwekhfs6P4wE=Z>qrf@|F2>BdzJ#LwU1Sv6-E!qg~pxQ5(4m*M3v4dhMyr2#HV zi$>XiC0zuJTa+64XwSLbOhS483gSfX2{j8o7Vzfq3~@6+9Wl&g3yh#3`{eG-89ln- z-9nZu2?~+qoYdiuMr#7yS@gtOD|IjUThlKEFAExfsk0th2JiSwUxL5j$-g~42IzAj z^((g?zx?GN8IH9Ke^C4psMtRzEdBFl((WS2OJ#|WM-Az!Njl{eZSdJ+M=uLVpT*bE zPl2ys#)PU%;1W{c(Fwn%u3~;-w>Lkj;+=A}^eZuW5d4+4^3jvY1?mcVX1w+=kMT?M zbN-ttm(Svn`H-`4tK~Wy$dv(%sKFP~4{YWJRPR?PCGa>t0Aa?QrSi>PD0sQBBcM#; zSG5?LOX9)Pn3ooqWx`*OJNm_tEi!8YVvoO*H1@wBK&wc=kmRm|EGcFK)LYFFmS&6; z=vd3ogwsNqo6rvv8pN&wJiaz~A$pd@B#YW%YH~ve#xaU9WY~l_X`M6?iER#*K}_1 z2Wnn@Xyu=9Rug|wwgxsquk9E7MLB&HCo;r0=Vz_uGcY+AS2va@{3-P{>eUuEy?^t!G3zMjN@tTPYv@|v54 z;ariM3^}?C{|GO;deavpWLJJd)kspW!V1!i73c`!QZ-X8!p;(uAbLXH%Ti6hxo3^D z>i`=3mB{VZLqoB&9IB;Q!nQOj~s}LmO8y{}$lf-Ef-cmkB0s_v0o~+rU6Xz?B!8J|7&++JBacILqUNW+<*`(L{`_MuC=a$+WD3O%eZAn8y*h4*kB01WhF?MYjpwf-&Z{!l>%D-5jUqIgBa|Z5mU8 z@2t`d4v50S0y5a1fz;D)b#a%SIEJT=i5bHCfeR&~22}J@M*s=l5sC3UG@=8Bkfb>x z+lUA-$vrpx+N-uC>`kKzOmK~BSQPuZDLlUT#+(U?EPk(%v4OSV^Z%t#>ZLm<%c{W_ zTqFg`y|qPOcs zILp^pd*fqf$17W3E+kvAPhNJpdl%NwqDBxfNv5*>v=AMg;qZ&8A*n?02rZ z8$*zA3Gk(I+n4#JSah^$T_+rq;rX5>MYsyTn)$g(Qx$UVK;X07j4fb5f!7L z8xYID{d>X>s79{%>+dqdt7PfNLShN@E6z> z=xR6E!De~!JkV|paPbQwty};U+1OLRIwP8yiP+?Ivh6O2to=FDWM3Yh@$ZD#0VAq{$QE+E%YBsi4NVl5}o_kSgUx;|$*eLzCLlE9dqgo+xbe!svv z1UfnS-ONC93zJ{v>gtC-(~l(R2DU!KU+yD@3`m6LK!A&uCPW%cM4P2JV2@;57Rj!R zK3Be{`&^+$ck1hAaPy+?)DdBk4%o#y>4WAcY(@r=G<}d%i@o3IcaGuq1@-wpUdo6m zVy1|iA})M`^6VOvFBxe*e|Bm$5#jbUkVyTvO2=oD`$lE#9X#8{Y5~dkm8Ea=6pfHy ze)voNYoJ`dIdyr|q!kG= zph9*mxHx733d5CMM32;mR81$Hvhs{8q)ZlO?hWj=zb9}OThPO4n+vBN#0y4Cvsd#o z@flWchIY5wZU;vRchMw)X(xz0x`pyPE;%$o8Y_O#$H6J{u>63BW!(KOUuO=#IG^JU3#-E|P>#)3Rt;R8?1!E0M4$ zL+A$LfUS)LWNj~_`0?Pz?Tqn z25uSDx^)_$CHARJ(_rRW=id-k-#L|qMc@Bh_ri(|Vk_<*>f1)c z4Jf&ODN*-+2hKd4%_qtic|gqb2U(`73+PEF1sRFICpXELQOIMm73nHl_%-bJsQhs7 zB^3$heSjf#kg(Js2*JB5a!G51;7hQ4oqb$QF7Bu3s&_J2s7XFq%+4|M&d%D|^LuQf zYMt8cYMT&%E$5-vn+a*+YMVqrIaD&CaP2icpjTYeh@Fx1&~B>FIo!lp*cn^+3*)$Z zjU{Jj!|5{+F3?nN>H);ktTW?s{BCnnT*V-xa|VEWc{VIaGMqtSm4<>Hc1l1POB0>>>e|}XL#^*Ww3NqN z*rum-E%G)%%V2Wd@3xnNHUN0~>WWv*)RydsFdw#YaR{--M_bhKGRp4SP@J)db+B26 zpPaE_>_J@ZT*z(1p`OlxfTO4|yViZy$`;KmeK9X}vQ6EY2p^fF{~$L8+Mrnc;>vwD$pND-+4J z&LB^*957`?Aq_mmfB;B9x4++7G^IE<-KB4ZMc;xgI+E?u)Fr?6S!2fGn@R7_7NFs7rN(Vl*&iQ9y(Z zsd)ZYLQs-14BTrSA2u4#T}$?M8by9lP#~iftMppp4zanTl4_^mAshv~JODIt*0o!Q`Po=?ANALUj$ zRV1vp>f@@B;Vu2R1_#9nUY`IX(2)E9jY)q%HNP^d5$NWIaW1JJ8@GW8kW5m8Ea3#% z-xa}(8FaNgK)dExu7xsbL zSYrTf4HH{3x#*D!P-mqEnEKP1kpX(c&iFQ{vE7^6n?at=fX<@~^{kPgXZo`r&y`Ll z54!VUAd4SRtXue|an&d+7y3aEV2iV+=2?=V>8u$9Fqr*ts>9{3jARv&ZY~KO4J0gE6V#d+pbNwhXE6|mPp61nHyiO> zx;*Kc^5|RC3~E*o|CJ%-tMM-0u_5=l6&b1E3Ak)NIvv=%;3R@KHZNMKA@c85@r}BZYyJZ4vkYOaq!>bPf|AD(nnb|4LJH zD0R|w#DQ6U^b|6bX?X(#rDnescsk+<3leI6_VOezMh|!w{RID14A7=9K%02od2;K{ zh;luHs@-6)Ii1{aSe4#@Sljf$bb8lqiz2hB_p|hCV4sMZpPH}Q(lGWp>*Uv{;kLP( zvofsI0gPt-*5t{LAipS-S4p0lb$qXHs?Rk)&AB|dmc@yw9?shL%)n~3iTXiE$GlSe zd|K~P0ZmWUWb1d+xpMwI`!OjZSAqpvfwhD zn{qL;B*lE$RB)IyXaQ2zdi9_f$*N*RJo=i7mKd=)pOF>orV7iwjJ>N?*W?l$T^ttY z8C+m>_E>Lj6Jom#(L^SKk)`6N6)ymg`9b3VLCf%sUwuIo5-}5QlRgvQj)6IFE7G*p zXn`?x2Z&H_K}-zfoRKajMhf>9BIEZmF)R`|<(^hMJ2PSZaDtA53oXz5PEtlJ%39rD zkD`m=)~-z<#1rsYbW`n9n~rREvPClhyF4p4#&sV^IuJ!v_riy^6bMKp*fsh^U}JBz z`;T~)7j~K`!zIK@#xA%Be1MaVvZt<2g>ZPjhIYy+~#-X07&^m)fTC2o(`3oDK_! zk&b%q`)LNl`y?%i3A*-A2;cMp=Sn_8-$F29^&fOLV-CPuCc`*}DevWLoHG?B_?BB? zlDrQ>6P2+B9&5UJ$760p*oij;FCx6l>oDA9#udXzs0r&?ez~X2l+`w$6SbQ4LmMUv zU%5U6;Am4sNem^>nNl=Z$j8++Sm@ZLMzPzKOh^_q1aaR{&^dV| zv4F^(A5zLKhzRF1H88w$Cxcb?TO-ETFBXS#?viaVF08< zm^;DO+#!UEvu%ov$)e_AvI&TOtw<;>xt|HwQZ4?QO&O_q6xdjD@z#Z1`_mF4DKwEw zP-$(13uV}wJuT`5EHVqZz2WTX1e&brf$i0*v)fs8gCYVJAJR!(rPHzwwB+~`$NxG6 zAj?7SMqy0=q{JPRDPNIH#RO%GuGMd~BrJin+Wcz!uP4Y+_||t}FgmudGj9sx<83}} zCz;>@5RPObV>Q4Qy7AnB&X4E%nv?EdhO8zA>?hNvy_Um4UkuQ^*kwuqpsw2^QF zK*E+Q5W8Ff*njO#=S=&%_lBFK*kw{?Pr+|)=M4g1bl{Q<%~Wxb8`K9(>qry#U~A5W zT61Z|73VDW?b>P>H(xLaS^ogB>p&28F|=8^*_EiN?nBu}7mRU5puovDV&9v_0ems% zVLd)NON3dM^-zvO$vLd;Au37=a-7d+xAd1~o>E2$OAks|c4oQlghVYwbLBF?T&xYw zm7g;Nr13VtoGW^$UtivA%RmN$+Z#UD)71{^&d=dU6r5iA0>8sq_uKpvc}zrz;2BN$ zIjy;=yIR_70#<(KVl#_B)-yN$m);HWPb^Eeq%4f($^S;U^2ba(by~8&n#njb@d;@r zV*yZ7>lzov*Z-&=U(0BGZTZdu6rGP=c?*xb^Ao4)Ut!5T7%R3IImQ{#qM0$5OxyJK z`i*u*`~b_&OLfA||59mlw^VL9e~}h-$%W`ZCg1qcYFi2*bhQ>yT7|Kgk=xb0$Wk?Q z)TTq#!k|Fc2^Li0N@qW_jB3cfMCmz!4l5P0D8tUrF737UY%B$9as#n{f0{b~3o_)CETZ3i5S8aC1jVHpRB(2-)Qj2U8i;DRVxl2{94!a58o)DHnt ztjdsy5lM_j`QAYqpD>~Oniq3WNMdvzgZE4Ro z6tz84uF1YmejoC6!sAr%I68a%K6yZph~}H!rqjie_h_JOa)2s*x~Cz!f`cCkCm|o! z-5pT9>Q2gE1r=V7-rEM(!Ejg%4^35=mnkIs!tHM`tQ|n4Eb|fK90vgF>>(O=CjkgY z2SWYB%#lyLG4#99gtB9}%HTpWqrwY%T;NK_p4IkmM|U2M37;jIpcQL@n@f>z$0DO` zcz3IcMGAcjX}!&|}!T2~H7t#S?u z8|(MlwKE9Z`2oS9>>H`onNm6k6vosPB2(14!dVVrCSq;`Tp&`_hDGH)owhFbja)6i z2d(pf-4?dpD=4B;C~F#wESnPH+RS1CW49&aJq5p~ajIwAn6PUu-HXb35ACd94$T<^ zSCioR!q0bp;(R&?iJvgl^@dSpQw?u8nvjXGH*@9974>%P1kcoS`w zbVc*!0CQO`Kum~h2!D&)5{;8S4>M$Wj;sT%b%JQyO#r#sEP&X#0KruABi~VthoTlo zL|n*0R%I$MD|n2Xlaz9{Cck@!Fw_(zX?Ty%G=ceYN~+udftwr4&<=e7a08*2)5nb` za$inU3Q>z#x(R^ZDt^}T>{-a2!xuQ$D;_v^x*-T{c=ht zP8!hH16AM|DHRBdC>KhjoK%AO(xQ|`!dcG0v25{po>b)t0z2IB z(oTA?@@0&7k$+(VQ+D(>k|Web)faa-=Sv2r9p}Z(I%ZkB&LFE*v-JXAoMN0T)4&6S zko1V{^QM}e1=7O46w`4pHXU~rnQ0$WGhTuI05#5wI*M{SOx5sal^IZVn``CXZ{q>B ztxmV~;OlkI29qyK_!6$csKdAk*@mxUW;Dl-ayJO8T9*B*JQD7si+MQv)O016+qgDz zxkq}6OrgvdsatY;jgcAa+)kDo&F~E<`w!QnBdw_jdvIP0&n|YTRS7#x^dh7Dm)Z0f zfh=l_0K|-wyo)Iwqhms=VSqechVu7+`w^v>skp=aGKWm$ea%(i;*Z7ICB<1R7NuVN zJ+OeNi}JE`wKZ*kF8q+U*1~1vD%eG2=VTcTfy!sW(# z*2wM~Dum#M-=JA{bHRc$sJJ?@3o=jYM)8+rZ0NV*SR5M)?&y?n1G0U{fJozBmSh_j ziRSTt~*~9{I(rXk1 z6MQVY<#}eDdmjh0hlV3BS6aXO8;g`?q43MgTc+N=|2_354&QzG-4EYg4>e95 z+m+PH&|bv@TvUj^w4#4#;X)cu!PN_0PcJt z)Gg7BT!1nWVg|9gezpb<5Z8oRR1iSBOeMBI9mH=o@q``eS6%J17pv+l;=-PgOPUh&IHK}Gxs#(%t)-;$o4RnkA z(dnW&=7D5=OCfmcQ-(7a%m3kc$b`g$9CJm{$7PziCdrc3 zBG42bn~!OLMPd-)DBlyhGPlv!vu3EdRX1j@sSXZU@nR~x;_^c^6+rQ$58DoVT;LZ@ z8mF#q<&X+;em^o1Zo`@q5{HCm-qmB!VgV_)cZY~1f_7B^a&%>hI{j2a_j-)A77}a} zM9W10U2X-)F_1-x!CbO^8vsxZ%|q_4PALQc$!3S6F@6sjy?{!x&Ix&mt4g1|)FuI6 z!5spSd8KYt@=`i|`t91h-+Ab%UQfPRLGqjyuit^lXFQ|ghm*u#>e(Mj7II&kJ1Z`0 zF3b`+81&?%2o#Knu4&Z9fhY227@>nT*c{9;OFicB>q zHW=p8?3C^zVoE(^(mOu`e;*lB+OO%7w2(Q-YO#fxcB^W!(UTQXQq64wwYGS52&{Gb5o$xwy&`dkG}HnnZ0ZgMJg4 zd`kgabqtr~#qn3)=>f>Du5va7ME2&kRK7mmv@e@Q@$I(6IR6hcFq?fwn1&*M`0jc_ z8&A*h>O`~lr|O1h7T^h6xAJqN-yR+?L_gsXW_iM3ha?#1oai_+KYJFE2rEASgTK^~ zJJCM&Qw9DeZ=+`@Y^NHw_F!9cs_m8ldh`F$^)E?|14))BITggj3If!C?Qdnp)JtQ*(!t ze~j$?`wSfv!+0X8Mp8<(mwzmU>BQGML02h+yg_0lDns@4w!m(PVR+W2>|qKA>Po;1 z8ph$F{qjj#nHTl6dp(@6H_ zF^QNEuScwkj5JcC*-9X3#d=qWkIK6b^X;vA@&-?!d^5BU4M)r*g8t;g$L$KV?RJF8 zXWRg?)nOu5m}N4y&n(xNBDYT9Q%ebwbIoT?|wPOax7ne!kwy*=GBHL zO?3}NqZd3h(nppbHw?w{>e_q7iCf4hV$-=ccR{9wQ^YVi%>Y!8o4CG?eqgHhg62q6up$g$JVoDUvII|IYbtiYy(f4c2SY2d^Xv~L7vKogqVCXA-Dn-*SVmyn9> zbc5U{2U9rSrbB77z;zjBHy6_BpMyI6`e;6gmoBck{A8oaxX~A!r+_f5o}qF|2BQX) zEm1CT_@;nm4qvf=T*ggDGbA=Xn`5K{wDQWdkkTd5?>s@0Uz#9^L=z;IXksLSR6P<0 zPJHI#zV@0-sntM)C%v zw}w|eUT}07Jc5bpX-NV;>|rw8amExLoHD}0(nhpE zg|+zG9k}1+-wg&U9gd#}gcWjv6Fh6vi7Y4lL{?((6~clo6heU zC3%`hyY86bn!TjMNS@Y`<)~EGxDEVt9-;{g@I{h{g^ zWak%1G-KnH-N-AxOd}9-3z~CdYXs@yvSgiftWNpebMy@>`Y(@KHDtCR*oHVf^$R5M}rzz}@z6 zl!On`hQKp43WbV(=GhOY!v&)>bfp27?Rr9Jd518Xx+Ywe+Gmf-@d3TLd)n~~lJAy{ zvl`T431@7#W7kkPZ)l>7k&Y`&eZC0&D_(%)L>%D=%4Ul}=&222>{6|!e}z$IA5+Kv z70(qHxDC|Juw8EXrA3FPkLhYpx2MzJv!V_LhXOH}LU3@ z$fqA9186lcsISdfe3Q$c_#QX5FX5Nf`&4I|C6yu>@Clkh#n4l&xZR*@wTV5qh3jFbjd5oc6K>&^iJO^($0&(2w zLI{`UO~UwKTVPc_gtYJ;;a)9*FtsHRC9R+yPACZmIR*xXAe=PW!e+KGVQhrZG|QTR zh`cB;9&>u+)S$snNRO1+L`iID+k|_z@d(F=3}N9m%9|r4l1ep@5R@w4s%+b(KG-s3 z1jZTnwwH0tBg^9`(MF~Xs2!IRBx$Je!|#lX_!O*|kQ>Glj7f0mhlOtQz*dY!$3 zG=ASa$=K@%gfdt#gbx;=$(tKmQz1y9D{e&T%=Xi?-o_RT~pmHsS?Gjx8O7FU6-0&nx+5v^I!hYB%Uo{vyjk?GM1)3 zH8K6J(V;I1ZS3R-R$az^ z{W**>jwOq%vDrn)=+>@RL%>g>aKIA7SgoLp)#AyR)=fsU&qUTvs%YP%uOIJTChkKg zg=ji-`9sqIx)U;I;!|Kjm{Vc;>qTDtcV%-}lL@Z^8@}_{ztPA{nAZUar7y&S#5h42 zh|HVvU|4Y)sR|cE;ew+iOlZoLA$)!PQL+llo-#fZTZ7brmVUvgDkXx3^dPb>e@t2d zoHpnf=h$d1G8E&vx#JldCOpGuYlU|gA37Bwnwv4Vx-%>p5~OR_lSwA!qyWtA>)N$u zZCvWaaOg{3V3L5hi+w7GuabBA)1c>2kQ3ff2MY z(WO$nR~%YAJZujbuk4CZRf?P{pNb}6U#h+HsSYJeq)%QrbBJQ zgsNr1PqqM0LL8A!NS^)1kH}Sk5JJ2gP(Je{gB{Z>S2ChY=gr*HiG8Y0i$SmF^0Nf-MKXExzu z*}hcnbMb#r5KX`3Q5!DJGE<%p#|lMoy^APe?b*C)xz`1e%(^aZAf4 zs`zh_XZ{n$I2q&;I^#8a%_OFm%dgImwfYLSQaR*JA5l%$ZqFc)X!2~8*u(v|U?q)`+p;rU=y2^RNvhZAUP;DFIrI}^bM5Nzr7hk0}ms(x`R$1Fn&t2N;lR8@u#b$2nc(rWSwr-B5 zZibd__J(ebc5c>Yu6%jcwdZ;E-HC^549!dI;?ED+TA$f8GzlwBeIp4OLiw9@Ch?&E zJ^y(tA9mqzqjl?ag2bK-p&NmU>P+2_BE#N-gwfQ*`?EUJY4pE z^H{U{ChE7t>vwERdy165JO6KVwr@##9Qt6Hwk~#(h3x523u=igKa6KF(zuWBP+HMb zw||3j@3Bwe`{6g-?g4a}5$ycFf0CZnws&2IMmx6y9eduhbhz=G7}4U{^_%F7WvDgI zd*^KXbLV(_JMElK)EMvC`|xb`IX9JzZuS)=ADwcN&)U`WJ0Ai|=PmUKO>^gcNn9?U z-vn@(=5g2;#ZpS@4?fZbEkU=zrE-NH z^jgNL{m$r_p(GIBBcxs1cDlXE&!_xw@4~;YA>LM#cwj)%#eTz?^+zoG zE7~8i$o*mSQ(OVI+EEa8RlmyrkFoT^|D%itK98^iNWiYIFqzC{m`t`ZKK(3LkxKi%ojtan zc(&{1GwCIKzCI6#V1e<|HnBcWSN@}eBa+4haA>5&5R#TNffjj2ZY<~Xl<$}N#B-4a z$mrqo6uU+&r@MTO$$_cOOga+*sfdTN@ ztszwyG_6M{dNcw}KI#!%1dthCR988-A4)}@7h>SsAfB^jGg27NFc;{kdYoWRfCBUD z>+8;gHN|}G_mN%uIe_VdiQ%hT_`nNT08F(#V1yhr!) znGCop0`JARcM(?pyS=jRok;rSjcSfc!%c+d)qa1nuaTro$Jus9VDugFW{+VXMF>Iv zY0gzeL_nxx1m5!Nu${mJw6jy{)g~DnkU~c5*Ax)MvBFHlU)wtT+?pp^;AD&dYtR!q2)4qKp^!)lpH2*YKBWWHGjtGGbSFrHMO9Q~VO7w=!#0?65{(VfFA**TjcJDVx8GGIDkUK^1NP!50Pk2FTYBBu#?Yh7DEUL5Q$(eg0XK?F@p3Y zsG%)lgs8}D3|K@jJA`CQS|lrSjI5S3h`2u4NQQ@INTC^WXxxCtxu>h_F@{V?3~7iM zeiY%IbX@fVefu?w-Iw!#_7Tr!{orvPVatKKuAzef3U-$!tjPidV+jl$waZVoc!H5D z(9^fbPxE*ieruCnz^~XHrGD#psIi5o$cuiah}pPI=^B5M%^Tmr`gxekpJt@)^JmMY z>}Pv_jmllny|Kf9Kijdp4}*3zFAo1mkIL*U?D8`-)kp>EiZl!8SAOQMIhL_v*m)4n zRS|x+^8t?T{v>;-FY)(hb2x$1I7CON=T8kqF8}|ZKhp0&c9ee(e@%BoSGctLFZ7QZ zeNuX`Z?~9|?cvmnF)U!H4Zq58s?``9(J~hE!y@WhWdXyDtRjfi8a3E&Xsie(t=kv& z#DP7L8pi;F!NV~YWQl+!=`V}8m}Y7LXFJ3T8lAK`VN(s3t(MCHFz+G-=Ev~t%A4pc zNNDV8T0SUtqfg?UJ&$~TjL_GEe=WUx?r9-y`enk{!dQFQJkz8s`Z;ttyQSj zDpYoYsin)+zGV$MT{E@CR$(oJM3+Zm_4Vy2IRj~AH*a)9R`jnUF60DW#G zc%^YL56DuZA2bM}$F~MLzjJPqhClP|HlPw5aO)+zd>-!jji;ZwW$35_U4C-~tk%a| z?c00j1DEz~gMLep2l#En!7{;Eb!GHSazw0PxGI|96FtRQ7GWE*u#67J1pW|#9Fqr4 z3Nwnh5|Fmj2zh#Jm>ktB)r{R3_(&>D0@JcUaGqJj^qO6p7QX+bF0%!Gr@ZXHRaZMH zk)A{NV~vRctuMB3e1#ML=)UF4zx>xf{zsvXBQ^nqu<{rDW5CN0gYonb#*+3J79UuK zpb=bO{bZ70=?9N!3rmockK@>)-=Pv*z;L)O`QTEJS8#TO>A1A zcWva6ES2iX0ecZ_12BBLi3x&mc#B|~qZUF2v?{P>X_40{JXj|OudfIGo7RQfba7MZ z!1&MMbhF=fq=r)arB^ambSs$}3*&`5t)u<2RSxjYDA%+v+_`kDJ*1Q}GkoQ|kWsTk z_2N&qWN23h8NoZ64roi$fCS-spcB>5@ep-yx?MELK1<$QBXa1c#q}&(Y(Zb6e=p+r z)ip6V5dva#UB)^(U3&_HX9bgGpwa@SFWlPRepw49J(?yLAjG#8O|U7EA}Yjyp;an3 zA4q#6Vr1-E7K(IhlD>7}?&SlaDsPd#kIj$fI-@&2lr`>|bf!*dx-2bXDn#XbVbXT1 zvszUg+QBnrNNb@AmT=kOpc$EhF?^G|cU}}fQE_@JZTP?b&po^HjRu7sOASaLr}ft@ zCueC|Ne4g7HfQzEbd8!} zU%3>*R+$ya-kAXV%DDTCH{k_YN%}c%F5o(I5Ro;;E_O%kK9nl(>Mn7^Ch*_vp^y4b6yFmWk-{OS5DCV96~P!> zo|1|It3YTViX_XDq8EXy*vcUG1L#E_4x_Jn6m--Ba|&O7w&OC!F+1kE_6~2*atn<3 zh#42q0b*wLn;!`N2<>TxZFj?`1;!AFjvN`QRgw^x$uf*1KcUdmq2F06Gd{I0!o`=8 zNe3k&VmLfhSbg|si^+RE99@A#@XgHxINso#mXMPhU}4KU~u%hyF)3 z7DE;Xu7zj2RVlu=!)3d-(p9e0hEdU zAb%65ON}oizRGgWsrP{jyU@#kFMm~dv0n*LUbsG`n>7dz^7rZ&ATt`@&IvY?mwXqX zoDaN6Z7&RZ?#iSKh!B(kjIs-Ym=9hkZ0+QQ#Xky_Qf|1y5-4r!H1>Ppw{9?8g3X#! zYH0L$00iifZjfRI^1}xCr-ddHd}dbVYFKAngm8)JhYsM4im!_~!SajRb?s@rrM7at z)x~-BAVo%TlE#t!&lhIp0zuH zy;K`z^B}%sg)NG*&!8Q}kNzl5c1m+hR?*3?S_NqGWLn{3B6`40kG|iE@yM4ptlje+ zGGQGP4Y4Nhv*Lca-G#A(lT1f?J&ObaORAMN30n>&g^^TZgbnF_9uiyNQA z%0b-H*(@a%_u@z2x*!?KQeNE;(o3sQZ!UehG{_6quH|iFLc@^`!}(>C!dX3JFprPJ z^BW3dK^H}_8QTn#lc>&oC)Gj1IN&-E;D9%iu5{k}#pSfHBV;Zv+mMt1@S{R9)xBQaj=+oH;4bSQQ?&ZWime+kI>2$ssY@>&|fb9TLk zSyhD)tWy(>b^*VIOU8hh$Yt-8ww35IvV;Cg-pK7_DsinpZIPnd>onSk+YL}eV8~Cr zwWzQBy0wrh3}Zo*aUDdVBIZO`=`M0^ADIYv@%nzjjmd(vuX-UFs=3zPizvMop%LsG z>1B{}J?Jk6^TNj?Ov&ZA=yHdLg^$`)=U(jou#5h@ zEPB^B`Dyh>3q1+N<-uWNzgi^4zJ~}}B#TsDF^4z%ZCxM_0~Rz$E2>eE%(}9g1oq5C zjcdM3nAvY^VJQDNjC0MNa6gF&I7=DGQ}b8R-5B)zF0q%`SYk?F}IZHo%?3 zc0QCoYn?H4N~HlM(Vxfq-8e7eI5{Abb;of>es>wMJeu0KkI$GF5cOzxVWxNcilO^& zra6-fbZ%E(&Of|X33c0zk(^9Pe=7wj+fz__Jk;%pvy}&Yel$h^PObf+CNMYp%ASIf z=|X7XTGBPBBgnRez6qoADU_(iNSO5XuXXa5I&s*Q{NXs|Gtj`+PU|oZ`-(JNb2zS; zlYIGd<(_6|)4Zb}rOjz~o~K)+nY!EW;kOQ*4OHoNKm4xJthSXRBoC5mj@Ap{*mz=b zTxM*3{n^Y9Oa>+@C+nP6#e>Z5hECc-lBzDtI5#~U9CTz?xh>oJGKFsl+627yHhjs@ zT5`p=2+KYT*cM>|DL<%e)9T%GmjOfFZrmxg#*LIVYPY4$zO7T;q7h9xLv`gNBX?}9 zx^h1rVvx=VGZ1bm5nR>25v>@RZ>e8$HT0JX`}1@JUdZk3 z6uR%kOwD-QeN4#oxk90-S>Z~TQ6=GYEopmt+N{Kh>}WU+eh0lU^Wl_JZ`!)%R(THh zp;-_xi0|p@cA=z062%T6P`?A{^zK|5?vEY!*tL+e==)dfqU)#u)8of?UD;#bl|9y5 zd&tX0-EH^?ZA6B7SI4Le$~v_r1Ro%XUJnQE@Kam{xK)nfbmwjcym2o3mF$%fQ-&%J z511hIg^A?Fkq)b6FQ{?aF-xDrV_bH3Ib16$?;N5-c9x1NbLYF{Roxa&|aO- z5y}+iTD6J;chx(Jdhj|bY=^97=3Y+IImB`;jI9I-_3IHgY6p=&)m+Cz^>tbrn#VfF z3VUn%Dn?Det+zuxy&0PM;3>EUh3oXhu#y)>qB5^Y)Q>Q{rat{>&kJ7H<6I8f54xs@ zF5UtEjZ6O3W*V838=g>`irA-L%wRA#F2ZErY3BG^`Cb(QI`q4pDzB1(KW~T}tyUVd zk4x{oSw_EjxMX;)eif7`+1*DI&O((*oCclr%IZ8gajb8E8gG2G8#s#66Dvob^XTIXWn zSm_&MCsgHXxz%|#)Co+>0y~P0m-+^KP*qkZE2qxgmw8tLhW@nD`pD(~!dIpHI`kEO z&v<X6tUilq2wVzcxd22g53tqeI z+PWVvTut;#9igF$M?KcP4ShS;mG$h*oCDM~SjMO9H_BP_nNAlnR3x6?)gA&dtZ2&$qn9;Uj`GnL(J zIX%YRB1B!zm12@h`L7&GmdxtjG9W?uIL_Nm$Gq50I34qhDYaO%2ucdbp@ z%7gu2WNM!nh4z^mG{w~mbqy&^>27gFxb!0R3H$F~(*nM3X;p5LdIiH&wwXv3WV+Rh z)YEPGz>+-O4|Q`xRL#%@^xtS`lqX%?=}q^qDz9h0%&Z<+PxElEn*!r*Zf2Yj^pI66VUdk4^8Th#9$ai7R$O#GcBuk!gqer83N~N&~6k;M|?@&PmnO3sbxK zs8<5<4PIoq4`N#L|NbQT`KWa&hMAg?x~!FC+^^lwyA^}^S?P)myS+);?(I@cg{j;? za?QK1lxcD5QRjM4Go$K1xB9cID-JXF2jTrU`^tsf4S1PKA6f4;v2VNQbzZPlU8-fQ zB~Ke4hpt{>VJgS>L05|P)!wj^`QHduHbsOiR}w%S;6J`GKnTX5&Hat*})X zXIY-9+|K(&n+IzUC3m1xWC2Z=`ZCkj>VQODcaHEDpcm!dZ6l?~Yg(6jv_m~~eYB*W zT$R@cb#^Jth1hTu^S~o5W`e$q4r-5h?=Ie=72o_KS(e$=%E!9jLgpKc>c;o6UiK-j znh`1d-)Kt|bYCW%9t%!a!mKbqfZ;p>C!%EEU(nlZay zb5XB!f|53n%>9?$Nr$C6>9FKGIE2!v#ZoItE?Glzu@eu=ZWge7KMMdNJJkhFa4a-grOV+BC$He z##Bxhr6L5sUk~ylt2w`4X_7YSBt%g((1l>jo-9OS{eDI5n%}RBkb3BiLKXI@UxY}9 z1&nAbwEjVl85k95Vocm&4BSI*F-9i5qERItWV^Gy6PPF8?K~@gh>#pqB@a44ZkFEZQWQ(2T#S#F&L4K~Fw)VPnSb1jg4aC^(m z_{vK|{Jrs2GK+r8JAV_|4#czLpZaqMNscoVNlMS?m!GjbTR`^wZ%}gkXspuBWr>sQ8lQ0D)<+OtD zrA?`#FC5067}3nrJc08qe{)9di1E$C>8mF!%409~Uih$gv^sv6cfE{%Gq zNhon2PA6(vAkaaszF6cW^b^=e_*r4DV`Q4qtZOmo?Bv)u#rXsp9$w>coF<}?;I~kq z;7Cj|lElotoesExqZ}mQM2DDpLB-+MF~$u%UQPBSl)@yNwmiNZv)0ErrS-`I?a2N} z_p4w_LNbSPg?!kIxI$aCfYk=lOm9+ciq^#ttnfL87HaY1iRxf?y3on&aebiBI7qlhM?4Stg3RB-0kjo_t0R)eL03>1HTo!W-H|B=>06l3*RTwt*jlxMqN`?v z*UlMrjW1T_Oq7DMQi#+OY{x#jEe3Un`#K@|?^2>~lL=#?pq&x;N0aruW)X+ilr6Oi z5m8Y31yzr6qQwMSVH5_wm+Bg5I?Cg(2j*aEayK8a6sbGZSt}f7Ei-@kcL&X6HGxg9 zV&lP#>8mrcMtQT*kv_!j&jS)PlKG!fcc&IVImrxqf~oV=4rl`%utOY`IF4LX3af=u5cGC<+8kwa!^8xz z-H17UHNVeILEQ@E2B50@Q#E^^(q&kTaR@!ZWf}yu_)QxqD-gh9JE0221ovY!^}%9V zTQCn_Z7gG`#gVQtxsU}L#l*SjWQ^i5{(l;ia(R2jnn*BzJBFF8m<7G8;Hc|@-RKg- zDuqoSfL*&i2_D{RQ!{NcPq#@*5#Nb_FQ5HCEZ{`@!56CiSH33iuhL_@M|bspjl%GL zg`)6IU2#fJVInQxb7##E5=L;$Y2TAoU5072L_{p7yho!nzy1uvZgT@IXX$|Yv#i+7 zX#SKD)WccG{&1s(?3ZsuTWs)~`kW5YiYH z2`|fbY5>s(1!nP|+kiP6m78tqFl(npzj8M8d@+0pCul5Cu$#CH3>9}y9oM|;xe6l| z5=eM6p>e^&kmi+);SpvSbz3fY`6RT;B%H-tNUe)8x}$p|@(3oFR1biL^j1=<=sZ^S zdLD6ofl;zwG*=zmj7IdEd4ewYk-GhdwYrZsjEg;dwEk$nqEZ=<1$IKOo0)YI6xEe* zTHa6s7h{NuqUtETH}mcSphh~5QbCj_5P_6FL_qQogd3Gxu*UK$ttfqjlaJi(e(<&0 z{2PJzz{dMywUHRvijI_Elmh?x%72bSZEwsuPbOx>HYAIw&r-(AaKWqo-1PU|er5fD z#hWF)_S)ADI@9eK3`M)`Fu^^D<##kXG~qzMKb6f~~JqlwQTq z++dVUIA87XdFr{&e#%|5b|#ZY_$7`J`qVcX}{%3oRkB;lqEHCs(97I*17>b`ZMvG0)H5Ud|65skeN zv7|=$H|MkfUJ<2V?qOJXGof*Tw}M2xxWrZqSzKMPI1JK#sHEW@=7+bL!oXhs=cakQ zaUNTP^tt8BNDAKQqMX1mHT~RFU+eRi4}U*@Y18nl$1$+L+?Xg8t;sJjq5b*6HWvT6 z3?l#KzZc=V2=xZ49xPL?ovar&az*daMl)} zNMp53oNu`=aAjMb=Ou0tI6n^06OZPKM;cFnODFIn3U_Uhtwt z83J4yF~@%~`)-#et&Lpb-W=_xDLt;DdSzZQOlSdH_XWo)WC2iEuXZt$h+%HDpp8aY z*VmY8UtfKJmdy%XTe5aezm44wbS+W5+Zx}sy)|@i0@sFX+eao6fPA}FhFq^LE-YW$ zJ9~5ICq>3PON@vZbED#Tak#l<+R{+NJfe z>l1dP7-)0Na*VJX;+r@kEl^6$mCXsd#>Dlh8yi=?GFqeS_fK6XorzujZVS7&g*Y1Q ztWeD6WSN;@v<{8}^yy7f?}-lB80&%^+muy^D*KI#B0$UnmMgC~RU%M*b4UXcWg-Zk zFf7Ly;Oc6ehSV0kfor@MBLhNcoKT*x-WCgLQN&N!!oqc4$h0=M21WKYE_$+7(@>%t@Liz$k%q(t54Wy?yONwqVHPUdxkPzU1Qw=z!1|nrxI69-6(a7X zW01bT__()w#R({*pDvUHMEMYpplWw%ZLouuQ{3L{w{?O13RqCv+yD&-GzYaw=t9}W z%JFMja!?r6sNlP)W&OJrLtc~kixnAPE$B}C=RkHcwWaD#jr@(qwL5{RE0^7=jD6Z3 z9&BtMwu5^v;4ZnYIeoA_5YM6J44B@#=#E8)CBMPerMY3DY-FpB998MlVCwJ&wF_R^ zMFmbQ=rOkPjcP~TkFkNc{}@|@XRY#QkJDME4&6CT4+%c)m#2p+-9mgfv2n6dE-J@) zD3il(Io1?ST4OpBT^Z-7bSt2Km7kpjQk}8V+z4GgivIiIK%1Z z;jaxBgcvM!K2oB|Q+Qqg`rRJD>-K;?x5tB5zz0m5_!%fauwCag<`G8A^)tWKH zsk&2r*>?uu#)w z^``EMqoOMeieB}pHfq;;^pd;XYaYAMI{rnQva9Fw6iZnQG)KDV{c;0aP~>I-WVti# zu^1U+F(o9C6>J!zT~M)10Zhd4qF1U~#!EadT5wZH#q2DyS8-#E8*4QNKMZ@3VK9wJCjY z0@V2(wB?Nt{v*xshb%GkukgIkOg{o`i{r0?eCkUwCf5gFq2-IoPcq02lb)wl%98lz z=5{Py{UGZ2BIgF!IgjA&21AV~f46(n+32CjwUVhsbEyRwrhTw){Bk%Q)TeHKP5}&{RuvO z`Z+Xmp-T}LY!(UIlWN+{Za0p?Cxq!gEZJ=Z5)}Iz1oc1SO73fcE zx@1)_nh9V=S64Jt>$Ot(CdxhBa79~SmFrtxJrly`$-KhfH|Md56vtv#hT&1=dmM(B z*Ur<@rmSQFlaL8VEiA+_hhJWV0H1O#jEb=(VO`DdZ7-2i&{Fe(YKIb?P(b!mM5^$NDg8Ch88>N$fEu< zHxu4Ph?bO8JG>l_GDB5pn1VEIW-CK&b@eY3yVnaBFZenqp$>C6PIC7xtaSJ7o$lWC zY%Gl-7bQ+FTEUKCCc{7cH(gupWeM4G8`?Zuhw0P3TQ7_ui#^oWrAx77oa$a->S!0r z-d9tM<@Y8(oCUm9)=MqYO|o8g$ogK!O+8(zy)eGLp$CKDxIJ`d&@0~6L2#7_xz?J*3dDr9Z;B+_S zIz}R;5)m`-j1a}oHFSx%&9dd(s#Ay<~Jzg{mxe2@~9ri z^S>uum;3G7W>hVQxF5HOF{)(PTj?E6{9*brZOWnJO%Q%DX0AoNPKOv{Gt-l)7|XYT zu9oXvuvH;4?dB~oKb)-$kn9D806b~s6QvJf{_5Pn^^mIZ+ zw1Qj%nJib~gI<>tSsYA=f$T7WYhEKcnh0N$E90fO8p=;6eCX2#g;CalsJx#t4nwH~ zEpE@;LI^QqY_bI_#)Ap?1sJ>(r0vWw_0nC7ASGhrF+lnG6r?}mD>i@yNf_k9D=Far z^2h&=xOW%o!>MxA*V<(Od> zJ-YI%O0lPNYgn4I0dKpfQ!xJdIhL#k(XnNn$%x(8i1HO;%E(9iFRVO?x^Sj3i6c5@ zKp``m1}{k4->)$_3aPaDV6Mn;@}qQbl?^%4%&dZuiXlN%-6cHO@3+1I(6afbQkG~H z;|>GnfB6#)u=(C9Yn$(rt)Y-b#8)*gFdleBKQXPRMpZvZ zGIv3mus(fb9~=JT`8zhZ*!>=x={0p2;~CcU8PJK>n5-IgjfEkjdbIgVXeS$p>H1#$) zDPxRT>4M-KPH)e_hk|UBe~M}2n2Z9Cdz-)`?LZtW3R;Z1B^YxDHCAsv;@EiAMud%~ zmv+P{Z!*+04x0@TQ1gss*bHRl8Yu4fX)_hPfBBF9AONir@9)4UuySAbxYb#<8PvwIlc6g-qED-;bOs?REi-Z10+CKKta!v;|) z2#OoQp?bGcjdU)5GTl%!T9dlqz*;dJeby`NCK2goESRwolk0{OIFeQlqELwYa2k%; z4ceDcoS)0+k;Qnho>Ea{2z|r~6R2^`P41f8+qKSoCUia3-VgP}f?_no2j)##^riiC zc|Oq|0GKKyTt?up%^kdCaz?#aXQF;guR}laL}q%8nJ*2?rT?9N{#ZKxwfOP#P(qtX z4S?H>s~}Zo63KIOQe{wGnx?A^ywRR;%Q_B8lB)swU_u_ZBGc5k%mH3EPt}6-p7!%k z(TpsN6vgRUDuus&ZRccVfCxLDN3yOkq=G+5#{WBuw1D%&atrE05xrSs8S~?c6_HSH z`WXbUMHL*`Ti{BT_#rEef85R({%JS zTaud6`HAWI#(zKgFOA&F-<0+S#r1Ht@C{;2p$O9~J6?hovJEH~a8J)6eTrtH)9oO; zUnJxlyrl_-@99peAXud7FtXJ`RGBEr80(I-Zv(k7Hwp3N_@DHsHbFh8JILvL zJ*Jpi78cPPgvl1gCoc8TZWa+5Y(lxYRwg3Nr#;kDnbu5ZX@o;+2X*CVlCWB7;9qhMyu6v{~jx6jVelOT2 zGmbX4j3H&+6oxg^mKvOGa3%4{l@d9dHrlOCO~|`9_GO});>g%DfY63+qb)9=Z}eA} zEZ)0h*zY~x2mFrL{t^)N1(uxRwQQEGkQI=#QV7q5_fA4#TL2_w=agqIM9k{-eq|tt zt?Jg1q&>_#Y>T~Dn{V&9`}SV#M7-lp1cSi^j|y!m*zZ9fSAp!SK=P)9OJp)l#YF^H zqh*=fD3+wpc#jes;l%xOg^_GMt!=d47!!WrmKzhpaa{TXjGct7cQxLle#F;@{Pm&! zI*iKSsXpNf-Fw_IwmRpj>Nl6s6BYG% zrrT_|C1uHnXETnj=QOsS58^Z2bITx)(Uhv1Yh|@LW&~j+<1lH^8dQ#fvN3QKDw$>z zs&p}jdrf8yf8~aMsI303Ml6;ie~^<3?}}F>u?N{ zDaiRiePoO*XSweNOGa8JV2GiaLeHGQiceH|ZKh?;nVY^jspQWM|2d`#!C2mFj?n7O zP~~Q<0<-L%&b)~`!!q|Ar|Dl9;9^F>gZ}gf6S!GniFMjo;s%5zvCcS)^8QTMEyleH zl!!iA$XGL;2C;-On3_Xer z!h;yU6WUZMZXTZK9h@Sc%@Edu#XO);CemfVBjz$E<+K>;_wrjfNtv@7tAyNM>8B^MGyHplun6 z7>g=UV&_wsd95Nu(XV5mDqoO`wTt4#{MD%NBY4>GDGIIr@29EBF zP(6l6ELhNrT~eQ7BBJm}w>K07!_B0g0snw1G&bKPN7$?`>> zGMzqIb=VFcFc#WigKEq5WXsj0o>}9>not@DY<4#@FIaB7OPM#CA+;7zAfAK`tWOAZIaoH+#dyXmFl`gO_&ZMg47!v{>hqR0LD)SY7>t z#L@*sLJN+Oq7u0T83#d`(F@ZeC}N!j#K0_&F-8L02|-v)R%8Y3SA-SSNiNOnPlAB# zjqT*C%9rNj^a1v!1ha?NLl-)x;)_$_lffmqUsK{=P}{hWNwiU2Kh%kODiZ2c;T$amq!t#O+$!f3H)py4W#7h^)A zhZP~?^Q0;qR}gO?zq{N7-8gG$@cABAMGejo=Jx7Itk7O;uWglNrkpLNFR*+G;cxa{ zwnYD}@@IM7a=sFw?iVn;XjuJy9!}{s!tz#;TwRoamyKt-gO8wPV7iRw`dmDPo(sh5 zxjgbGmxq0F(NNK6hEq-s#@>VqROds-e^?h;em_`9RbYrZFw~V`h+04*Y}OZiTlGgg z{os#q2_A0up~^I)>`MYQ;}lg{H>9dIcugDbZRIx;_|*I*dFLzcwJ`KIEx(4ldyw9K z`PVnCb1=EV4G^N*(yOt|25@Mz^VWGWuhcL$=xN_UM+jrKKlVCq8XI7NQ=DqTPh&0WCi-JE~5zMfF8~V{ejI1 z#@^L2fYyQUZ+LA?YEm@yatXWSLCVXgUoMd^e^B)1(x*$|b_gk6$-zPYsz+8~iU@7S7{gGY1!8>or^c=FZ*%XqRJ|5577o6pbSG$@ztb!Zl8doD z%x#DFg4PMrlEdh2yrP^2T{L+w|MS268xb>P8X@&r04CJ`BJyTAYr%9-+e!ATX&A_~pvYGa zanrT(_qf`xoH|nZrgV@>k-t0J0ik))UJ8t&phla4{ub996qB~AtP&TJGZ$=v@KPsy%yr4dFz@@((RYe zk9boW1JYW5PZAV$hSJM+1h4n8gd>xuF zI&ERuKgQTEGQ^g(?HF$VjX%xDOyswao60uSlbG2O#mn>HXIP2$u8T+NkdlHWC0Uk~ zlmUGj8Ww7qT8vPvaTc)R3XEV##Z@=5{?pKMJ-}MEb7X&n5hc$b#j#saaHp{OVX#ed zgpmnIUBMWObW5ON-{*KZlQ+;3UHcC?sFNUu$1TZpg?d32xhx!4;BC_r*ktP;a2Y$Z$ySVEOjzc1d31m>a|1 z^(qw(B;8U;zWGrGk)!<}Y-QXQDvM-g{Z>kPOacsF{rk77O*u?`nR|Tjw1vTdH>NUE zog8Iu{DiAkgUK%@>rTeR3Yak9Mch`sIMMLx`ze;-gz57z%~9&! zU*&HLH={U?;3|l z_Vq*);?D!yiFv-|u95M@pFkoD^w-~B^Thx}NNC>B%)FxP4tiXM(&Ih{xlr>Vf*V?5 zNV6msd{!gKgU2Y6X5eclWF3@!ur+X!2YtP6`;5X*RE?T4-k=S38>@$$PMiSQvZ0kIS z6IsOA5o;!3xInP78PA*#xCMFDA;h|>BIywHMUmRU5QNw45+kN)SgMlsbjVqLs*?ka z)sSB_a&p0XgrOQV4QbFcRD-7Bl$?S))=+`Oz;j0e!;i2brXUA9-EqNKdh4MY(+n`C zA$K;Wkw-qjp-0e95_q*=-h@(-HlajpOc?BirnlNq0NDVv(T-ur1|h1fFcN|*6q$Mh z;NcM{t%t*$|IR`-C?`r*|A|zSKyp1~sL~t-u#dOfclM;uOoKpg9>c{u_~Cqb zmF;C71r`{66RvSRZHY?`D2QY4)&}g-1|NG3w|o?pi8fswTPPi$4HNceQOtGEVlK*hwJ;UE{t1J`R8c%#kN!p(&Of4~>j;1iGctN*GJg;CtOE8=@vVN z34l?q{Pi5BDos!`h#{zI3XYmIrSwTrnz3Yq#+xAM;mp?NbC_%dC3nz4k}6I;gOkxS z&|P(dja5hod9Xo%fNC7zBm{e_)lh?jG;$Nd(q2wmBi+vg1AqyPFF$F>pFhq-e3;>p%kiE}KM97C^^VQXd!7Ld zA2l9m#=Hvn7t^zvNYRPq1%FaPi;ZFch*({iU% zQk4wK2Mp!a9%}jTpaMk0mEhbT!S0d3+NY7)KwL&JN?#k(RhhxqSH{WKD2j|*^9;=s z*~$^gQjvh#S(gY9e7bO4LA-(d?s5}!8p2L+|AuG=AR7cv&-O_x{0l~Au4Ea0*VTYE^yyBLdWy3rPLk)ZmSZksji&q^)) zr+>C5*DBG3Ft=Z`Y}oy;|5;FJEb)5;rAGX(KNdK=W{()FKSMuP5Bs@#swu#7l>)|X^b{SG_X z{b8cp(rh&LZ~wKlCw)_GJPW~Zvj`i{jKRCcH=g@uE`PE@M{oKZeNw~dUGJj*wC=A+ z(KhyJ6#ZB7O)MNaS*cZt_TxR@c# z?@^ZfR*&WXzap0>q(juVSPTj2_gG*h_MYy=k~MGIYfZA%`>%CW5{)On7H>fi#kS`8RU7;JRc&{7i0;CCN+sk_N*9 zKZYKk!=+RV>GEow6%y8wQonfZb|bGZArhO<+5z!5f9!>0NukQRExqfu^jUkQ5ABsc zg&NF`^b4*_^s)cjuVCbjL9`s}721u}ng%~AH5}Y9aS_drBIc94g^=*A)4V?}9T*@XNJZFmj~s`6B{I7i`<;W|_4+cf_Q`Of|5_qNpi`Sg z;&t5N^hST;9Bq+DF|}T#M;l4N2+m_T;VHue1nl%le`qq}E0x2E)vEZHHZRD4ZnC_f zgq0Y9mScjp_#m!)5c1hA12t6o!XV<;!`v-QvRi}BV!k8DTTX(!9>C%te@m$(E5T9liF{>i1-P+CY*YIAbCb^G6Ma4Oa)<_w zf-pZmV6r95Mw{{5deNmYBFo^jrS`9S`~Jr5kEMp; z_lD!R(G*k*cB!||p@+~Y5g(v^%9HkxjK=D5`*x1@Oo>)&G6PQ=MtParN#0*~1gc=p z4Io$F*M^WlbB)i@uG6!E>D?c0l&gR zk7)Tmigdy6SZWA@>j6CF-Cpt!Lou96(MAu1fOco#<Hc+zW|EPd_Yx50 zHpL6P{J+KbzGi1@dOG@`k-*i9du|=d0PM#w{U|LJ6ti)-KIm@Wj92$z=ntp4Yx?vB z+O6UHzH_%`cct)IniTX!=Gy;{p&4E7ze@~6PjD4S;wp+-sT^2gzI6t1f51f< z%A^-I+7qd>3T#3W+E!?BN}+vVK2E8UPj^4DqbPlk2-fLx7c&NKT2uSMR}Fvx%zSdZ`hl$bOJa+NVoPG%-&3M==B%89m!U(mw}HJMp}$u z#NjyngLiqLWo`4~yX3=(J1DY>rNuJeP#O4d_AnB4)mJuMWG5+NyTzcp0Qv9+8zh1} zu6Xeb>l=ik)(hhZ#pY`ne-)_9^6ah=%15(8j)w>>qqx|NDF`;KOY^&ONw$bEi5pOR zkl!0p)W+sgLHNWNJ5#iQ~pg+S+e!m{YmvghZ z1qlYy7M_=+A1K(rbzYtxx1TGk@6&I3<0n*`mjO!hCC_^3=bhzL(aD<|k7tq9XPY}8+5Q?P|z99`nibAlFG0ttZ|5gwTvZ|J& z)UIaoZ^>Dtm{Q&xAraVaHFxM>+H!I=Qz}zTT&v0iY>IQ?WYtPhwfBx}6cOdV`M@{+ zr+}N49fPt|ad~Z1gmQU*ZI`Z{HZ=)ba9m4N(CLss2QS8&ND*gnmApT#&v4hsm0A>^ z1|lq%Uhr>>H7?Bvc0?GuNs59V@)uTS5b3kjbt>~}U5et~i+}U@I$hC&FYocB_yzN~ za1aHLvG-7y&sQ-TRm3Y+ntbW05nC=7#zpgGrr}tE1iqRUE{l@5&%p;}7gpGU0nPfB)ZUw!Jm+up9_;=A9`t13AhiL^Ky3QZfh7bZl1txW z3yzCcm%dF+F)mwOY9nLDM9LE8KIj7B3QZjaTY9Kj(dWQNR(K}@*1*aKv@^a`ZMs;d zijpcgN)}kIS`9-FHbtSG+Glt}KxdOI$H3QN6pQI9e-9dIj+amr!A(Sq0N+C6f(JBq z4OV*rE~jE|Vbpf1#%zqEq6kW@pi>NB@MnVNvX2H zXezP6ow1>u4{|G!v`Ymwxd4$>&+HsQpTJPRn#o6xRbixUY(M`;4^5nWp4K}2B ztq3g)b8dX+lrYcytj?FEOJQ1erDExlcMFz^rB7Kz=cN$BIX;4zhHI5D>#JWa!|)lc zM*+!_Wn#o@u`6fq_Sxq?r`wuJ(TaJnR)un%Y_~(;d$gH0z+K|lc5zbrZZ)v1a#VJL z={6LlAYy;~*TP;8v}qIvdYy=^EM{=?25qMmAjhQs{VxJqJp08&dNXbMTz9s9jk29X z##{A0x5g&G2SnGsi7w$Z^{!%|i+?eQZZdA5`+py-SfC>9%8JfPZt2KpXrjMYtN zL%Qc|fZFyVKw?2xOuCK2flM64th9S0iVJaf94gomgS(n%hD&2e5Iq%fSsRa9%g@CP@QJGU4ey`Hvp^ z8@eCVR&+nyq$yE9q6qysANq4dAUTr=|Ijrk5_k=&2>L)a!%4W=?;^sWgq$!Ui4GE# zU-y6M(r@_hULPT02-F;yz85WI8{!CNf4; z4~=ncE;4^lvaHSXOU5wD>CkL~AQ*`Yw!xR8^s0><`MdO4wl-y80C{22XZs!R)X3En z2B0a;gt>?+zJnU~yb;jhYIlcbqUp#=p8Rz6I&qhmiMx80xXX*gUA0Eswba!{)7wVR zyE4{o->&2o-0U)Rxj8N#r-r8I^y)R;#Mlr0O8rZ7^bXWa_?eYcoS*B32J-6{U^7gk z<1WU(?3m06Ky2uuyterbeKb+;6`pr4=8XMx+#tiH@|RMljLD!gbuO=1CqB}>A84fE zO$5UasDAzMikte3VMDOszsnD)32oUW?dW~plV_*#?Dma>`B_E_U4HL6^w(u<`lNtT z_}jAUzn#Pw3FUhW#`s1)5i8w@5{3j4hp}^V_uY7qzU_4Seqp^NIdXkUCu^Tl87j@- zrMaaPNqPYV562PB*f4b$`!kT__te0OZjLMV1V$%hq}#Me9HJVDlNbeIQXHn?+2Hab zQLs%+a7uwKwLOQP%`$jM7*zoQ!wvdX2-Dmr-yR1a*b7=zYTrK4;VR`peB}m_<$+rR zu8RXV^Fz-!Z=`jhAR{13MNcgRnNR;m3y)J#Af3XAu<@`0O~jH`sGV8Cw{(7HNO#E( zUG6KOr@($}i!>xPwh-X(`Kt zd(7Sx4tR$f`vM`DaX#^%@=_IyIk%#-@2Ju1JSE`!$neS*r!Z5Lf{YU;@`!Q$b>$!x zno*LBZMWr|1L|+!B)ts%`uxd`eBd{8>7Iuw83#>A)Q~D{XqFPl3k1TTaaE#nwuOh9 zK%_yw#5zDXhbd63PE3yTlD2SFqON&wQwUw4AmD*qZjFpHNJwms%#tn& zRGgot6_!G^=5dQ2x400{Ep^ke?3<2d-*hn5SKg!+!kFrv6&HfmprOz%QQel*|19$w zW7@wqQ4;to_t^-HDpG4E5Za%HCd8L3W<3P4$R-ef!EFF$-@J? zyiY$TG(6AJ{_!v8!1GL3b)3>wm`ZM3w!#DvAa9WEdv|co< zET`IMl-E8ca!Luq&78^+x@U8V1zIyJH1Np&Ih;xz{I|c{MXndr$(G6`g>*QdG@M1+ z@-14zWsKwF0*C7P71oYubF-98T*G!sB1U)lR3%HP$UGw4b?zFT`MRHhZCNxfoCg?Z776SZ@K2PO(KVgW9|8`l*fI1|7-1$EIR(;Fg zaf$Nq+dJalwo(4ucgbG{O31orW;P7ELLPP<>*B|ab6;o zA#0gMe&|&@>hH?0tcQ*@m9cRn6IMUv_ohG8D-S4p=;Td0>F19TTqAL*vQ&;GO5yK?i$PljG1dDVXS~- zV|VQhBMgqxewWNg& z#@@z_atQZ@1fLI#-H90ad*j(44XsE_KHYG2WW3qza~zJH?%0gna>pRN;3joILU^}B z@WbB3{+uA3jAWRq6cPSr9t(^#3IR345rz$;jeJL5!bm2WNr)*ntTwUTBw#U;`_0t{U{m|Bpt% z?+>Z{fBZ`Uv#SElvBFA$BPdqMSd0n9j32b=UK?yVrU?5d~D|TAO72)ILEi)+IG5pB~9-pwROXizv*p<11`;MU!ls# zd%v5;>t*tF13Su7tmd5W5X1)A#RuYUDUlSF=haMk=svHMtbk?8Sd z3!Gmk3W25V7g(zm;8Th(U(Erv@FK#IRzmmIOo0%>MbIz2T&n;n|6gHCmSy&Ivp(c2 zoz@_vo!=K8+#pb8>lbMGKOEQts3e5-covJCIG5R%bBk=7%>P@^gG<)e1*{i-Roab= zd1B5IjvAEkRK39ZashT`hDB2w|LW+9obor79>Jbjgwh1r82?mGU2Ih^OxD*A#;^9k^NdR>FL3SL+vjVp<`#DHQS&qdO95(M%hvSuc~)$4aNXe2TeRmN-b@gAj> z^wY9Wqx&7S*@qiHc;tCp*s*#p|W zFMk{PD_+g&K>eFn`O6Kq+@*fk%hb)-+r7)S8t;BX+P!-tyJY+$HA|%r6}gIuVAO{= zQy)c9mT{ClLiXu5oy(9^MY3yX_GKw9$mgwq!(X%!dGFK;rRXrss;h?O*9 z&c1K&1Z>qC=fhGaT=z+k?+FORcV*4yKAbwrS+s6{H|zFyy2CYf!b0FvjXKL!Mw3b| z{k)G~r}9h33d)~S%*2-Wc!6Gu z@bm3@$S(TBM=Hn0T{Luzuz@p5^P#}osXJhz4)X)YQeRJM@9?X1SJMi%KZz=-d+yt3 zMO{kwG0hdZ@%5iIy`B$K_4<0drhZz;QsYuaO<-o4VB4H5-&9upFeCVW#pdTl(`&kq zUu3K$5CQ>BGlj$=y3WtWO&I-T)}H02U=9al_{ z<#RA%N}B(~#{ZkZ_A7Ru#elzSJ4^tY8GdAb8kVc}I zb0H}Y%SJbn%pz1cf*y)!vB`E^tKWX4Em$VJDGnQR?8E2jmA_B=Ew_{dBiriquMU%c zb(rRVc;O#fjbNetDNU>%R8t2#xZ zEU!xSje&7NHjggiImGwBqodDL4muI$-REfuu`UF3Awc!!Io>=!?f1BbbPMs_#Ky_S zIScFOS?{2fYs{5-#AEBni~ONKGR{)T$lL0IQ6J)rdrbNv7fVX{7>lH#NXtZ;pDaE- zS!8yi$xF!Az8ScG*p~UQW(jpXlEJ#MKtvYvWr9sbwZ4YgvV;lv`|_SfYsgdi zO(1$gSiD&COk>q}kB4Kb;*9mlYku$=e3_+wu=F#vMoLJ~1T6N>&$=}lrSgUUR`Etp zyz!7WhXk7afD!Hwe<^*=x#?_onUI@%eKGV$e;@-p0j#kx9&9JIG>LNx)g&{5;>+Al z-Qln8f#o%yyfG$@-WX%cO@`y}jWep69oW5CZ83H{aI~=XA!I52k#p+967qwl(WK=f zVxtN7!6r&s8N&eRV;E&+lkji;Db>JUVhhyxikYMq(HeF^-slVThQ2`UCdaoUmZO4X z>8?n$_%1j972vC+qu(A>BrRlb$Et);zWv94D*}bN;r3;0PSPQ%5J79g$|A`T$mXPb zT0MaqQ`6jqGP6S4!cqw1U}oZqCxej04c!h!e2kc-oiWfd0?>=0Adu-1?{W{jMouOE^b18GBLeSSK z&IA3XA~R?)DbHSIlZ3^eZmyvKG?`~mF1JHuEtpc!P%6{*C|AyH&&+W&iOP7Zsu)^T zW2{wm9A4Ny%*dB@+Apo~o8)`EHQ_Em!*frCD~QUsgzkkYYT^MaeyO~eRN53$XB*+J z(lOmtI^tcWBi>aS2%peu*jQg;`hv{g!odKd)u15&tUy!0X$_2OBhm|$kB&)ou=Lr(4$uTa!F=d8jeOJv%IGjT!3ub&CJD*bM!TKeAu6&#fic%JxGvmr@?~ zxX;XR)o}I=Dn<#8pq3cK#2*xyq%#0Kpb6gP?+#e@EsA1es5Q)1v|4}cERvv86vDJ^<&AzuOSU8_Ixk@wiA zF>td7kuC(HYj7A}6b4JmyY2FbXr3ltYCkF;U}D1P6CCShltqBZ`Lu?`F{9X*7CfaY zAxxB@8Sj4(=p&b5=7QRZI6T)6?qY7e61WB;1V(FyX~By?)_@WbiYcC#C1h>JjMZ#b zBPJw)W1M*mbkxe`T!QlTk>h|}_1CdXO+Fc98!p!6A1KeZT{rbQg62~&k`Hw%B-!j{$ zt%`AlesaA_ulX;AG!<99$*2g`=zGmejDqbgUtnahep5$}8wZ=k_+c{wM)x zrM5CbemiXl8=QRz@g0-AATuytYK5?7(~v302LY|QaOsUS5(x?tT)DkBQDtan>%TWY zIq@3qvgt<(M+059f}1@6+DZNrHVw}CxY!u(Nf=DK7pCM~d!1Ntrb`*}G5`}&`$&YD z8$l~pja)kI2{C1ZGRXWb96$o+eLM}Xk(Il*cpCeIFUG|DMhNa>wkF>U{OXT&_$xrp zDvD_&e29}ihj>-x zpOQ^~{x9Vx6ho&2-Uv3i>6j^Q7ugu4CBDh`09YEqH(dFiV$*GU%Zr4J#fP`FlE+Zr zp(T{^XE@O!2@8Ge!hs#6>&@)Zn@jFPPfIW+(E^Ogmk??3MdkP&Exf2Wgmn0kOhUcF zlge5YK3XQS7};LFd$wIlxqsb1I3Awg^K?DbxP0T)vE2^+yngqU*Prco0ek%^E%pV2 zZ3!?m%Q)Pd3s^v?A9?m?8at-lBkP{*K}pc<+c2M-@%C+Up2wyqr6miFqHoZ{DDCk(Pz*LGfq(fm%%d;9Gz0!)+M9W#q-Z`bwP&f25$BM zXd_vTmOFepa+%Yod06^1-jz?>P`7Hegv0N)hsknwBqErvfBAD;Mznwa*MFcZr&Jd~ zXUjy$RGMz1!ki~)lK=)9_qfA~M7lf1k9r7EAw2xylxIXC;`3k^n{l4l7v>js=M1mm z^RGVcZekC3K*m+#I=sTc0D%@AzhDBI?d#Wg0Otjn>v4GJpD+2LX$zPJ1SV2}DJVxm zxQAPlF9SD9Q$?m8kr9SN>)_A-`sbFKit93ynN+z73O#(fz7&fPl&9-UO{5|Rab_5; zKSOYt%MdYgp7hTYkei(@v7&T?^Cbc_&u?+U#B;e{;LU!?b%Luy9-TM<9(gr+bOfpXm2+<-cV()|rKx$r%o*h|y>|RRt9TOR`Q&#PVI85)L>yZvgOm>QBiep4moFkh20Fg>k=KM6#WXFjnIZs4CU2DI` zH6(564Bh+@K5+V}KL`6YuXJ(F#CH>&Gsa&C_uK)cro;*`A)XB>USf&Bu7jIrKNo!qaGV& z%lppQu9XtHaivI)UOS4o4>wIF1C~fdZlS>!j1L9 zk_)TxSruxc7MFjUH1vlM-$HCaluD=I0`hGM0B%+gBPK(?-cZ&^4IiEk!$*qJ@Zo_Y z@h!v#L<<~)3&<8Y0xO6XA_g~&z*h(NH7kT#FWA5I)xnie3Id%)kI4hn1z0C-2?O?J@V$!#nSS?Eg-4bXKVVQX4tq+tvV2WUTO(b1+<4gHY zU0XYzuiWwq>0DqH=?x;BC$7SD*zgd6CkvedKh_>w)UW5?Y2Iv)73d0!5o`AG)V!{z zUG^{=fhD+Iq9HHLbNQ&NkZ`F6F@z3B7&^o=WqL+Qm&htbo*2~#aeWK|Z0 zUsIhrk5$KV9;>WBkCE>pM~Od=5U2CwFmlWzO6{qM?p9FM;?mVFeO=O(OhsKd%w1tH zwMujGda0Az$>dxAXr`S*;gUN#qXS0Ag7Y*Hn09`Z9&%eQ42JjO_CpC>TQhV1Pyb!c zP6u1{;ZCMxGd86gkx@FYtYVsLMB%1+T&`7e0G<3Be!u3P?K(Df@?NKHK0I?|mD|@K zeGq}{4hwmSfl&&-L6_vDk(x62&5o{w%h6IQ`6N`H_W_;0p(N(U?eP4l`!C_}!#6jB z%ZLmupvO$462#}GuZO;>F-9l9h5PMQ?Y3f1$p@?PW9g)|5RbNAb8yx(Mlh~D7zcX3 zLE21}@Jd_mG5;ylN&4@9ih{t@V;6{1?v(|H*C+0q7I@BtosRvN|AAiFBXE5k*N2%T zLu$(n<#4)F<3%3O0-;RhD>@}HK1fM@`dE1}I7gDn2-dkzY6i@eiuT^^l1JEhlX0C% zLPDyIv`B)tJ0M$C8KB)@0`U79tq&kiXzOF=YO3n*Fx8D{VbGK7Vf@&|U)ahO>+xrT zJl>GXGjeCEloJwCZrYY=yT(`uYBC|f%tp&-yTA(@?=h}}AY=sLPB$+caLPgJkbRRf zO(|^WluB*a{V*Lq>NlA(jUb`sQEXtlUv!YchuFqd{qenPWNwriFlDQL%2+HA#-%bL zWHfbTp(M?}?$3josLb`JjD4~#8XzHYnTV_uT(8?QKYo2gF;fhsjF|27)K_7c=EI{7 z7*E_`#S^%cGlTB^rb47FgjlwIF-Zp40alEM#+jqzyU-YfH3Ez^lWEln$Q)Q@b1oMW z(sq11!2pP0l`>;9H~p#U=j^kw+3Vr8nT8QM*+QL`*WKGnRxh8HYi2~-qAWMiNXbTuiUC)5-o@&heF*-DF| zw}hIEWcf63_DB~|9j_VLdV}Fx1yeww+R|N(Va3*{GcDd>#whhoeI{kLW;^A9Oc``` zyKYj+s+SBFvsc)CArd}9M#j+3-#IIEc6F@-b2`*RcjIzE&Q(PaCb=2v7H2fw&~UQd zzVdiUscO7b*lA|7HdRrM2N5l!R9WO-Jo@nb_q&9V2XIx(;BN;h-TWVzps@{(x zuTl)^s>f}(*DpqCO^6xnwV=`05KnL1A|tTXNH4^!d;= zX&s2BzLX1I+?>Gm7bh_oPF9WgJk~3i#CW}$muI!QQ_>_Bj~J{r3NQ~y_Zw4KmBCX2 zr+WvDND00io9#|Z4Qb%L&}F4nh=uboFUO&4>R2fmj$XHP;KVpN++cHXVrX$DEuA>I zp=ZKik_TI*xjjhD;=&V4$ZR2?KpRn&=tayQ4XH(2wo-OJWabTtY;-EMM#79IsMMD& zrT_e9IAe73RJdK57@gxzZ1hnNQpZW4O2EhGX@rCkSS1Ua%Bm-5L1LgD?I#;Tr;K-o z?2eC>E3)@(0LKXh<%Ht90}hu|3yiBO!lSf=+1-<`ZreH9unZO=Z?zQS{k5&5>tu?^ zdK_{EeXOjSS}i~so8J{HJm0;(YeXOGY3K6X@0O#OjM>;@y$=2EJ>hmAnC7>8*z5u> zI(s5M@4I=3&`{`BnsM*u*G7Xoo=oS?i^)33+czQRdSzZQs5T*_;$hATgO$55L*0DP z>BQZ7M39(=+d&um@*;uLk#t72yDD!_BK@`lkA3R&GrD%y)*lxZzb&&3EcM21;h7RS ze&5eil+Vew+n(Oq;g;*cW*ZF9kZ!w`1}+t;v6XU?GHxLAHE&zBg5Wg7{?VqzAD&S` z`hTpJpx8!@rU?G0KbJrMQU0@@BO9IK6Vfl0;#V@d>U$UqVQz%d~sAhv?x zBT85a6kK`WC3bG`Le~Os_iO~kDT|>%t?-JfN$?yRfkljkl$p&J`S3ttM49mnp&=s9 z&hzuG|Qt0Oa0YiX=(xb_a zQw&Qxjef52z47z4UHsgp3kjGjV1&C`S}9qcgUZw)thjbh3-V?u#XlZ(j(b!SK^U#iy5xdr}NJixReQQ z@ZG@T8QR>+sMpLDAc4tl9F{33Q_`Sj=m2~Shg1p|UL@NZm zdPA3W`;|<6GPE~qyW_-|PwT1k%en0$(=sJg+A$DQOl@mTKG6 z_dDq@T~v|<$MeerL^y!_XF7V2YBfJ z2hrbWWL|tU_Iq4Ix`p@-qI;jur+gjS#qvCk%eU3X9wtSAES7es6SUa%+xnC)+b-D7wtO0ki2N2uv~+ zg4=NVIs9n#>PbtFLLLv}V|d+`IA6moDT2aE6mzThkOWV|afGJ?BeG0PG+lirvOP6k zTnVbqPZR#|vgT_Fj8HZ#A%+Y*K~TCSu5DN9*3zbT1D&9;=ySnIy^*=$6+&2$ju{Mv z3bQb0u^fYt&!am`PNlRvf;ioVGQsw|V#K|zp@vK$I2zoaFS?;c#HD0ob>y1(zB$*q zj$=2)&|(QA#z!!SS^veE^j={uw_TzRug#&yEE~>j-K=(ZA37MtD&q~T;-p~YTng8H zfKj*+A33D=;iDOGX^_Zte%n8wZ2ANlz}W&;C%OGq=hMr^rYj)jNnV;~ij)SR6grMh zH0b;prq)z5N@v>HvSDCcjS4oUm#pDqCh8pl>665%g4Rn-fQ!oS$06m`qdiqck5z!I zYYqnMo)KRAlvpcxLociHm)9YUfwX6sBFs!qVqU0B^(!x3DpM_061yGF&p3O)46TW{ zMX8{`H21`2qGT?loq*iA=_Xs9QIoBKqX3Co13i)pG5^Rc?_zBl{q6wWN&$ll(x0n% z1Nj|g?a~e2;-yH?4n83sk&!L_c}AC40K}G>bvJ~>IJ9my*?KGe?Tqt!#?~7bd9mL1 zp05fb@5aL^%Sd$L#-skdS*c$TDs7L7bLdXwN&hj!dM6y zSqK^AqVngBrVN`Xfo!1tv8~hVYQI(sSiUJOr`u&~*@QO}S{W6v*n}iiQ{+pCQrua- zh18npFAXj1ZacH$Vy&`q>`I8PNdp7nmSg>19^o)W_OHL;`1)YSU&rW)nDK(gsOlS( zYp}r$4x4voKX51AslNKgN8_t@;Rb3EW#UrCNL&+xA+vC5nZddDnwa-)W+Y}Q0JCKO z+~|+y_N-YwTRw*_!X`REn<@Wns`ay(x{uhLfN6#+@NRJh*kl@#C17tK{F9BX3Bm8N z7_%ORJsq^*q+{rQ9krDa(OMY8mi~;aVWFBLM>*+Y5kl80AkEbS(p=eAfl1y<3K1=- zVjpxde!}<7{E3L3=?s$*7G)mcn%84Orkr&74de!8%Gf{`1eC>&R+K4&1KkqP5On%e zCs`LsCWazR`cN_Fs4t7Ld<(&xf2bGF`a@6u(6TE*3lyQHDnmqR;fVu;FlF25xMlJ z_!eRVA{QM47m&H&C|E({Vq@TjGJW@sR(cF;@JQB3kQf~wEuP5bdhg?MO#P=4>H`Dy zg5})3ms!Cnn^rMK-xs7F2ml{SDCU27`OAMPKL@y>N1=E@VqU)_Mtqzrt@7FR$^8np z{Mg9s#@8~OQGF7?s>c&oyJ29Bvk>QXD8~B5@o+r!)l=n?3-nzW^PA14%xX1dcB3h? z+f14D?&aA`r_5$8WmaP;Guuijf6rAw&K`)fr~Y^bGZq5<(bI%k!a_xU7V9Fj85fz= zw#aOzMP{)qGK*o6+3bqUYF1=6t0MCu-5bWxzFglLMvN7SY^HgJi{R$C2X5Y912-S) zyQY+}7q%+bnP8t^{_@`nL9uSiDEeh?x}I%d^0Dl>>G`g65-3SLbS+RCK!qAcMY-C} zWw!K}e=h&2d_L<6@QfPp9CCP{VPk53F0YY7yNgMQd748>*O$MPndMyh2p+o4s(G;G z_99_19;`K=htX%oq`Di8?)(Kd90&|&x+X{W9)pO@N9J@C!16N5%)$V8Z2|m(L16*I z_0SJJS{5V{{HeT2`>+LTUVV+=W>X5)IOZ{5@y=M@9S{9SzCzikc%JS|HbxV4e9K+6 z5c=-hF}+w|^sx-xlh?|RSNG!e5EI9wIZ7aV|LXqv)+~vT$>WgT82p8`Y8aW&AgrlL zBTgYOIfKHuoneNX2xq;CFgEz+{oRC2d8Af6qBh61g(Q;UDEU3EA>BfJ zH_p?M{P6yT=0?$ z_F{#4Nd znjf?>Kpm-y)P<_3>ji?|O!zbbe5F&`E;#k|FliOx{k3(WVd>WN;7vicx>bCv!IgQZti>;c87E(U9{N9&94o(QRCl=?CQsrbZ?T-jFkhh} zloX-mQpQa2Y8f}#YTLsbp1dGmaRiz89(1Y*hD=LGo;^S5n$S0yA6CUApVx*36^7N~hd$y;qnyJx9*RBczKkj=8*@Q4;y6C zS%oxlQbPJ(0SmaybEm>VZ%##~#lOxLV;|j@Nor#Jp%)?AnXAW;G{g#wlK&X^w87(R)5#(g;g^?xcohHE!;y^p z=WCXMlvoBKF%6t!yB@A|B)amzcWOC z+;(n+b2rarsuPDP3p{}weP;aqF&~vs#@-(Bu!LZLa~?jj$3*zkP{CmU2bc)g--J&- zP^ZJOb)8N?m(5%X350TEJ7SW6S7+?;+co9?+ZCR)Rj$7!bz;Hc+Dc1vZ0ZaV3ZJKR z6UKZhlam9;cCL%z^E%9_GzjC)pZe5&ex`>W#OGX>=!Q&Rr9ctk@6JQj+!&Qz_y-u( z-=KTFbHXsA?~okYq=L=&_VBz`-&e|Aytewoi(xM$+BL2$^BOQoTkR1u?hjO1-aFOM z&{lY?)veX(;WbVUi&(jI-NKiK0`R72r==MGDYc8JditW|HU%Ib_iLH;R(SG)(ZuvqUPJSE=G?* z#54O9Io9cAW14SWYG?@e(KN+gFUe!*rW-`3V;#UT0yz3<7ezU)BPg^RPS4YK>X)~= zPchDee#vQhsX}`3fL<`1mjfon#jboQGTDlT@D6aN8^hRXDDW#g{>qNOFmAIH{rOFP z8vQvBDqQrJwz^ih+LUBj?=Z<%IGBLX&p483XZxYWJr-{EE9JwIusfWjrn7KdL9~)? z@D>l#qj>NPPobC=lxhd#XGs20mTw5&12piU2|nU+eS(NzzgAzr`q!`U^(%h;ieJCN z*RSyPYx+8#Wg?=V>IU36e~HyH)rnMFrNV_}P7|5a&R z>4Ne5qO!Xlzb=3HM_JM-vL5^|;+TSg!x0{g)K4K98Cgci$fe4N@z86WM!LhW+j*yC z_y`t77*^AW@etF9k?n${s4Eq*AJKNm3*z`f<45Ef+ujxfI331V^^r&x`y(t(=z+(j zIavu&5sC;i?~&5Y35pCM$ltMHBN>ng&B^QGn40}DD)pG_A3@NO7Xqg_gxwqg!9F#b zgv0B4ia4ej;Xuc7c23dWCIE`@U6cFR>l;d3mZTfKZGOax#(rA|tS9Do&S#!-Ob05byc$^=?BeZnSOscJfEi7y9 zTUNYvQ=q$qiRGG`lhvCNcfGB-!6j_Ze!(7>VSV;`cunsy_Gi^*ju7}%+eeo3DW>%` zjuJi?RDPw|=xb9|x7X;%NyI9_*x&1FZ#(q0iy$^T6FY^w9mo28q1=btZXeD$cjIG& z*XpJ~AuIrWjzu4l=;vK9gmM?V=k-&M=I*-CwY1x4e?qXm`||*;iXd!Oh{*+n32ly_N_f#+WzE=V19IUd|r5QF{@=WW*wwnyk8ane6U z%-#*|mm1@*bO$st!9RC_VBA z?fn34h!F9e_FiQ)P57EC252j6YnqkI4>#Ai`@>SVD||=Or7ecQ1%joF4T^Q z%BOve@sB?J#IJ(=M~BnX1x+B>^={$$jK1b8HK<#L1v<|+80^xpwbWAb^EyfWF^Fw*0+yTEE+ zUYYRhksI{kQI-1v%^VrVG)U%6mB`V2PQ7BXoPo4kg&4JfCS$h-JX+2)LnYA`!e(tCUHO*j2@vblYcA2HJ>6!N&dCACJ_P81^NfondB zoIe~Ij0|>b&`yRA@2|BN?~qI7fAx4xPv=AR6nct>>xK$Q4di2uKC#H_ql)*s?;&+w-2a|_HD8tKznoZJUUh~^cK zT(rQo-?%GW#8N3g`sdUDGDMUJ&+GysXgV7q@k4{2w%)=*0gG??`>DUyLWZFMMQH_2 zh-XO073CX(_W%t%Xo7#lHzQ}8D#9JCF@gyIma*k*>9lfHmYMQ6-;!FC5aC(h4tWNt zRCW<*&#N_cU86g!>M;C{2GzkJO^n=m6-^W49I4-VV^5g~RGv@~hG$j%Z0?xm(P%XL zMkA@*yjChVueHj}YpC3?sD^qKKxrRygVK!2PbdT<3_t8<;~0L>{g-Zk*C2kTz%_!f zOzGJakE}XR(I8CFAWUArrl?<&)h{=ATI4>rs(ntbzDlj$6RXGM>2dTbXxtHLGcw2+ zUm6voM`hd;r823W_4!jad!Z(k10^&%f%{Sv#-`e9JFp5g{YI|F#;U-nTGJrLGkuu6 zd=AsDeC7;K`JJ*Gb9=g`GM!J@3SnO)@jMO_=IL~WsYW~HyqTZ{gA=rrd}LqGI;3iW zqJ`y&cB)&BDcX(6TMhPcyS9z>BZGdlY9?%FhupubAwxR1!*IC5n`rg8z~%m`&8TSk zD&xUi#(Y(xP?lF+G_fy)_;`^BQo*AAz72UzkT5)}))|<=$M!{%v+<4fb@-8dF3{c_ ze%O1%kGfC|w_{#&SH|95V*K&oxsAIch|Tr`COT`iHon5zc#-Rc&q7%U*Yj?UB@{kS zUZ3&1*RHMhXq|I61@h4)2>Uu->*a8z{H<-bV6K$8*bXUN1;&%C0?CAI2o~|FV>brB zLp7kFdVN=z@D^6D8Pu7(eYZ%w)r-VNfLHTwF`Poj8xLBmwVfV1v<#3_gdl7Xs zP+R(4!O-toS@ji+dOcv%m*la}3wg}I<2;>UB3d_rQ6?DU%MpPw?DYH7UD^wW%Rx@1 z2uwM53QmETAPf_%uNCAY4wA8#+1yMh%MtKhpR#?{$4HFwum0pdZEL$jy~yvJyM4-x z%~SXZGq!Dby3)I2d|0)xM0v=+p3pgObbI#$Yf3U@2R0UwEbANGf*1Fl-)@b#%H8*;MVfwVKP|Vjp@}4wPbb`k+>IDb` zfm{1Dn!$Va80}V0Hq_pGWm9@Nkg#mBRqG;zRgX(#(}H3IZ+Q{eEq_2^`FkGq|2@Y1 ze}7b#qm&U}MTRUg!s`+f*gpypzBgm7|03k#U$`binZ{TqDcn6Dz(U1(qW>I=(@ze? zsp?WrW`Sls#jmQ0sT7)GL<}Wws4+h#9N90NrV0Uk)K%Z^{F_}gsHeOVN6@!>Pq8R> zq9ycLBRQc5YL$wldZucWj`vPviAwvXlU$$$;d6Zs5*z#gDj?{W%3Sn1gaa7j5QNn# zh8_zxA?pjzgb`Ht)fp9UTh>v^U&+=Lal^Zy_ z9f>jSdf@G93~-hIM3(}FE~G%-BT&&hQ9n9)dOONrnVpL8A<}KNUnM@o2^I{#8^-hK z4ZBl9{uLKKQek=fu(I;@ftmg0yXL2+wxq+9Rp^u3b(U{7X5y}hr&>?ZDEIo>)n_)H z3pYcTdt#>`sz&)+9U3gV0F_;MSi5Yn%wB2Bgfdru<@`NDsPpGke26hN4oaz2krt@@ z64pj$=BnEa7Q>Sj{DVuL&uOX5xh4iQ&iQ+NGd}WZ5$D{^z3u9R7b;xL)#DkTd5-M1 z$xqEFf1$1M<)s&w|1#)A%y>+H^*2i1r=z@Nt|yFQPeQ=i8) zdM9ijh`IXdGX^C^jusD=)lbj*gs?FBoo7Do%h*2jT#hNOgyEURuZVTbp7k+#W#a9R zDoBgBG$i_x7^4oh1&D|^(>0|j^h-2MH}79>?RELlbbO8MCPXRjeHnKAw?E48Pnjqz z1+Q%Mhs0!LMTto38!h)}e%#s#QvNoiD|LxTI6piZ2hx zq<$&b0!;whpWz4>9Xk=O*Qh59JV%G)3R7Q#dDbF~Mu24)12X}AJj2N1;TR!E6>IQn zHC!+ShU-BMx>)8Z)9>842iIx@Ghq8OK+`jP)9MU#iTqlf`~x<9*U0dkj1*Rlyp!$< zw`@*B3~z_dMr7@iOe+y8LpyNZ6KOEdv5QF}3=hc7~ zl(|SAE_t+IROKYq%MA+Pwl9ER7w@0HI{B-i|LQF0U!8>etJ6S#brNE}Y3kNfYLC&K zk9l*Pv4yb6Rdo>to?B?g-RAjeEJNVfGhmpqbc`V~iLoOuo0TFv`L>Y4%D2+ z#WzGwyAw&LbRy}*mQkV|FEDSt5Ys~lA8plOGhi5I zYsosOv>Sg28;uGKHmfyNzqrKe1k6XM^ z=lXKbOF2ULn`4j0?UgXkWNF#5Yl(H`(=w&jtiX7ZSDsFBQt7m2TdhLCGxKBJk95`; zYOJ?3E`m!Z|Ef3~x{uR!H&it)+}ww{%QG(AY%pjg1W$11`o!(tR@zjr9R#pi6`V{( z31R+ivV<4$+F}KdWt^vPuslZ>;Mm*$gs(T17may?A!v4fbvu7L@FVwXOhn@+FvmIc zIvC{Tkge7?5GFo1!p1q{bCrUPn|@bR7bGSZ@bP0eZ*sPBRe$LN=1^NZD3G zS%%J^G5EYIZp7sonlShdZp$;F8Mj|K%L`}CX`SomE3|0Md26nZ%7t;^W*A@B+P)3a z=JGndHsif==VZ9LwRNbB3pe-X)b1Q9iLSTc$##L^t@GGnuuBMEEUe`KqPMTl0L;W>j{CR8U2>p4B<_Md8AVFd}Z{NABRga&Bz#@Y08pVrRtB8TZiqELmZ_0 zF&=9t;}M@vb#*WbTC{Bi} z`W&WF?qaA$VZvxOwApY3V;qeM5q@1$X;31)4=vq~uC11Sl=0`wFkWjH%9)$G1^j`n zUjb8~^!c~zh`!t7*@vv0$ZXx`s9pl-lx4ClL3yG!*c1ANX3~Bk9osY-bgVFa; z&bwymoSG#vxQ`<+#_+6#g&)DymwNb8-;TR)3FkgeISVF#In9{~#w*W|Ak#&O)IT(d zLcR#c6~r6J?=Ck%H_pa4H=IT@&heh2>`Yie%YZj@eb2P>SEYzj_N5kgs?V*5pgoqf zF+okl7tlOUob4iSRs>*-T;$jfJLB1sb9?I#lU#L@J{q)tJ*#S;T~+&xs@mtIE?p2b z3WUP96pW4rNvUl#%3cU-#~#RR?fBfiopK0?&>$8ug@iG@=&9L#p7?0spBzYP^8M1W zl_NM}{XCpJgZZR0mXzJm=rf#*c3HG+1SXF+Oi;qv90pUVqA=l$3IZ*zPq}1`SKWS^ zbH5cyK-DBOw|HplV*iA4|Ezazg|Vl6b~i5TG{(P82o}uGyF=G!Jab(@ZXgK5v&fKP zO6m3WsEfN&{_>J;uvEfgvA`Xc0^vQoNr-{!*PnOhv(I2X`&=8(KBMjI^X)J{Qa{Ip zKW)^Anw=D2#-8@+#U9TL8`cJ*hdo{bu(Vy^Z?PXF(wZO0Qm;IWHDE@r&{L2HsXff8 zawricoQafJ%xGe<9Myy2h%ZC?Ptk24j(i(K>C`Me+U8ePbCim}Lg&)AU-Q!+q!A5c z51_w2WWRl*^E{k9twrR`nlm3}m-}}O#0|X4M4Ap}6>>v1C<5Z?=ct zHW<&taIObgO8I-!y(D|x2?x7#XN{i|^)ON&<%c$igVn znJ#vvzflo*1MzGdh-cqGJnIJH*)|Z*x`BB9suP5Xs6JnC8e(chgs^ZaJrmyXbvj)z;?Vpk%(Qe|S2>IHw}+^g!=SEtyw7lwNL>pWV*Y=R!U zA)av&l#r5YA!7@}aDp{thPgW&u}Y4WwgwaClZa{$<4m_(RZb(~>-KaXf2BBVhYI?q zw_|Q=g-AIeOnw8~ig9a>mH*8VzR(=)Bm)DhDkRrcrEp|(5|-+m0{;kuwcfMYEjx$V zvU9dtc1}ji&Sf#W2*f7%t#X72ThuiR$V#%^>v~KkHj#e#WgaKdU=oe!{ZM&%=+M*FTcKxBBs| zdb-$GZo91ddFW5oYO7K1!w5d?d$gYiT!L?diT7vpVBcT;JfMa(p~!ne?$PwDV>*E< z($D5tm1|}EIj>xJ%VRjcRj(`m#ARg$7L6fai^Z`X>U13N<3k5;?+xJn9G^bZmBXzb9FnM>-_`g+)W%5Yj5e0a(lbi3(rRR%XiX$*>dUt zYwr5F8Si7gfQR$7h)IVJLYpx7Kh797ZG>I1t< zB_8ho@JNFWVPKjrgu74UwM!^y6p^g@=72F}0V7~va>Cy(`LK_elIDZJ^nIb+m-oK* z;=PaeRd)RfVL-jo2uQ8>KBwM$4~h5Q=NP^BIpql3F25md$prp3Db5=)^e^m^1d^jl z3P^^{m#avI)e(IaE#q9Wt?5e`n!fDrue!LA>dV_Kf76L4!U*IVT$bQylfY<>2rv9= zAC}{ampo#;#CWlY=?L(}2aK29aPiWxmeqen=-&A8$xCxe%PEZA*{OH$PrdtZmsT5V zPTsLhG}}o{-igR2??hyicP#RP^`-e$O#m)9Ub^ULDary2l`gRwTtpwalZ;wem(N{} zer9WQ~C(W7wlo!u+wpj(?O0tYxOJcZo^IMU~$5jWP~D*e~0zV~Gvd zktD_JJraSGsEtcytza-zO^|cpovST-x74Shm9c;=^Kz^m2&FErhf_5_R3@J19_t!& z+-tSkdxh2A>)}V!U2C6}b2nuMv-`So;HJz&f9zc;yK}c#;Ebs1Sb1<=x}AT*g>Q_* zFNQmxp4dm~CGIb4oMF1|)GMw@x8h36me}nIk<&X&6N&7LRRZy7qcgEM7nG~QIu#dw z2sJ`n^jthQ41}TvTtjjD3VPe-5@UDw1gw>|ucbGh&`Q4&{%XB1cuo6)SGHvgDKXX; z-5G^1K!!4^hCzj`Mg_w7JGWfg$6J*4N$$xN|}E|OD}$Z6~BR~`M-%di{?be~LxDXwR5tOD5N>c@;$w6sSP(;Zcx>sW%dBU}4^42YvkizM5fiEo=jL>ok zf0j$~>BC=9X)YD7zIhi|PrfZX*x-qc$%^O!-0Gt9c26F>MZ3`E19FVvS=F%#Oi+HiC_YRbSMCouNp`d!xRVaL3=S~-!uGj7@eeBKOYtlL* z<9LQM4}!iUOdy}3knKsRM7<-7?eE`}BK&2(Y1U&N`X!9`=A;KZ7 zoHLBC_qjd_fh7-@cmzS7(>^PU zG4`+7>(9Jy!Z-!;=}RRnLMbjVfv_en2u@dAvy#UZlsrft;Iyk^G-~0L4FDNOL|H8( zc@9b&3bXqgW<0@24x1m5Smp3D287T3h*0%8Fy&&LAqp0rgP>nQa?0-K*K79X>(zbv zdQHB3MI-fG9yuQ?Gf7OSH@CK0x)BDRVrjcUr5UPzae#g?@5EeV8>WaDzu}tGg0VZj zR;e-U%xD?i9(H8UTQHDZHi zHZ5?jmd57b7YY`bm>Q}WrjBy}v&PpU53>sFX!k=jvJ85}6k-5kST@|Nw(ky&-#zw* z3b37b60>hxD$qoT5&wtM?O}H17pw&zSPyOxs)~i7?s05+3g7UTjRF4#;~7$NMfA%wRUX zr(n&`x}BO+Pc@q|S+W{=y{B&aULC+MM2$eruHb4sbB|RDR~x;KhrDzyggP7Rq3sw$ z+tIfHN#s(6vdn)B$vj6|yu>OGENNDU;|v;tXM9N^tmAeGI;nN7alW zHR9I!BHt@tpz3pnnhTpy*@agS>457RG~97SF&nQn2XLgCQR?Wd$HwQ899IIjHSQ%g@!h#?v zxz&naN|}dSrTnE#_|jA+{)NKl9>u*y=EBX{qKGo_{=Cm?;oR4Gc~4Q~{o3$gtybR1 z;>)h-E?gb~3Io1eQKun@R3n*;4QeHzHVMXUTIM2!)_Sxc6h6<1wL)Rs){naA36)*I z*FN^eZE8R3DZf(Yi$L>5(9V}_K0K?YTseD>5hSS-n506cI4cFWLm1@&WQdiPnMx5b zNP7?MWnXuCsMn;2dd-G?u3^~cx;qP6n5l9+fon_2tVVYYHM;AP8{RmnQ$*FdV2M>O zSYnNf>k?C!LPl~dHp`9>rishq(~o60yWrcUQ@%1|?(3O$nn5q)v@77s`TJ8<#DxhT z9(a_4aXi}v48oOLF}P~^S~Wn>`YbpmE+C~*DL!l3gH{G!p4aLk4JAEw-)k@4lMnCT zc<_$zS3N;2GWrTLT?qH1Hje=;l!_qZ<=uH{=PEZAB!zXUd9=NcoJ@wHlTdKfM;Slb zBbY1(vWl5O>*ol1QRipS!amwL_4kA=v@NJ{5wd9riI6hwobJdH;t_7sAy6V9FmT^^Zsmwt$VZk*t+I0wSCp+25BLqi|+neR1#==6nNPx z7VC}$Y`t@ECfyf5IMKv*GBGB$ZQC{`wrx*r^NDTSww`ce+sWqr?r&>rtG2qT&mX6& z>sEKyy|>T#fE@NKlEgfNC(HO;FS{-WsVK?a@Z`i&5~AtOvtq!?#9i$nFysa+Hy5dZ zd23cPuegUox3ynXsaZf2R-qmW0yZNM|C*YY6x&w%R)@7Qxm~0F^mIb2y|p4_DDB?$ zGne?x=Y zRNkjGvwi@cr2Ts3ip5-nRGv-`oPqaVHfG-i;u6v{kdLiF9V;C+h78%hTF{& zr8i_g2ME@t=>JS)#Ht+f6OR&bKvLOYlkg}4Gwcu2%0;;0DScVXS{5`MLLSwz)%?*Hn|29Hn zzW0VYtv3RTp58$F$|RfQI=3+A4;S{uZKDF$9r_U|uy47P6_N#zk>UYxyQ`*eJnG-2 zTTo(squn~(`DgDmL&HvOxuu*mhF8%9!+NIlkq}L>zp!r7HF{31; z!@am<5v;ccWvYh28=@rT<=Z}gDCCQt9d}%pU@Xh=sPugKR*3o;EDSg1e03Z5&z%MNF|7>r~GVV!Skthq8ed)Sk>P~wSYP#@lf%ExuF zgc=0{1@wVT_jQPcST{(6KU3?5jwTz}X8hQDw6=?nFi&`u$-F%AsPWpCn7l->-RvKg za$Mzdx*cwRQ58lAJu7o0YWbTF+T$L+V<6vPkXtl6zQF|%IVK?I@a&C2{+o|GUqEFs z*XKj@@#WRgsqgwdq_~@U%7TcRu_(;AEVtVue%&>Er6!y!&DT4zy1hXujGEkx9qfa%)UySUEbK!_L_$)9QD3i8rv@02iW|$rl^>LEz$I zv}+bb7(3)7dgFeW;WV$1@Y(9K2BHX6Z2LBGxb5Y zOgWEr^a0Yov755*9VM|%>h=%^x*rA0wD0gT&}Rd|c#P;a9Ysjo)rKZ-iY#&<9o`8= z?07bhgF+uz>J2uBC*1j`N?r{F*7I=P%`0$%(T!#N`L5gJoJqZHJjOhrSC=jcW_Edl zuQ$~00#8mZXI-S7x&u6sw3J}KnwMo-h<$k~S8?~DQ51~n(i^O%8EP;^sP4pBO<4XJ z`&&coC3J^Z01Po>ha^oqgLM10DywCjYXu(83H?R=;Zo&jA~=HAW?5q7j+T8sunP`q zvSP2r)7vxZhBB&5*1NGy@!uK9KP3uCM#9bmW3v_xGgoc_TU-2I+SoC>?*dN00n(P7 zi=ly-kI9Q>TQfuF*boyJ8!}WvGYbMAC{QhFw=b+`Pd!&RyY31y&=%>P?bt6#}CpRj?eJk!8vBYJ( ze)Ylr!gt7+F1z>T@3fuC->$ZvLC&pYv`0_ zj~8SQk}G8ytx)^=t#Y$RI#1m2Eo`)Jn_5u7>-9rdlkiMySpO}jG>3>}#%2iH6!z{6 zHhL&e!mzaz$^KvWRlw4VzPW0q*ndA@825u!{RdSuwDUhO9Xb z%J~KM^)_C+0?M{G>>qoLVcCjoj6>xyi#~AjUU2dra0+g43NCQ+lHgP)Sd#+gbVhVJ z{Qv~^`${k&iMy6Z^870^RSXE*zkpp!sfU}_^T`_w%MBcc^G;8G?`9b6|6rT2-!|UJ zQb5!-BwmBc0I2bUnQ0rl>%5j{5#IMn?5C7_N`*-0CzZpZo-exU~E<4B&Z9mmGq zqr-S`fjyRqef@uo5~6)E!clqq%l4QJbMekfe#^Ktgdatxhs$nna1{CA4_{uPK+J6kx--^~9TD5h+At+jE~E)TW1akUtmnn$Ucx9wx9Ql*>ar`^7n zn+WGjeHgLJ=l88}4YRG~cAn@Eu2T9`?I|*6O4~n%G>`~RAcXSrnb!`(X$%g5gdb_v zK4hQnj|61dS^<$O=a4H^#cPGcYYzoKPmSM~ylR&aGl9z|7DpPAY}tnK`{fk<37D@5 zF_z0uTuE2RQe3s8?E>^Ln?azy6>%iD2*@5S(wDNq%aLgk=d9Vbon*A(+Ae?34o6yT z)s)}BGL^!KYEk01r38TL!#=CqECyNL(F#m61mH=;WXkfkS$Mo08 z72m+J|5T^+XE*L7qrDAJDRf|WJ29Vc3LvwNFLXARx;KkJ`|&J=7)7Emu83PC^&e>% zKmmw3)Q@bEZesZz>4}4F&z=CeVVP$#%G)1WWm+8OpOno^d9`#Zxd89fK$JshlCA94Li+lH4^%7X&84Js}j^pm7hnAG;*nZt6vTQ`x z7OX4A)8jwZ#P+sthWD{0M(L1d8S@j2H`PE|HygHEpVXzLbc65XJ zxPIdbl-jEZ^WK`bEQz(RZAA{8f_?zo7hyv{4VtT`!s{%9R#Kcqt@_;;HA)X zv;W%FI?5l(lurCHj_yDk8DO4(6VtDXD7RoJJBUInc%BK>)i7gx4kWe}-BoBpqyZzb z&4w=k;iPon=M7`UBov6ea>@6}Z>YUd=(sTArL-TP#4Fy=-bkboVp1dncThe{&Z;_j zTmwsf|ENx@lU*)|2r5w=>$w`~&=lKbQwf5NoZEk*JnZb*wc1gU0zT-+wIbAA95M@} zP!R(xD!sQFt-s=70e8x+)*7U%l5jB+sygKbc9`}??w3I7E&NAWvye@akAn>2$WGOH ztOW~Kv;`HG&h|+ZpA!7JWNKFd zE+#GN9={EM+=FK1EN1Qg{z5u`?2=M z9o|n~sgkEe96%7Kux7`P#T4OanT!UM&5(90<<<@ghndk(bJFF8$DN28{)LsRwNQ4{ z%X>cjb9J^_4Vk{Kg_P3o#r^%mf*INyucW;e)*O2LPgS=t+H+m-zV0+o{OuHDcS|KT z%oK5l+>?jKH+TFK4Pa{VfSbtizG~ZP2o+&%I-qRm0`B zPNr&WS^3pIIJ)nqO(R1pTU%0}+g{Y2px^d=OP#)6x#@{fzm1|rlfGVw>B(VW$$d)2 zx&~$}ODr-6>YiJvFARSg*(gcH({GmCN0!{;P1#RNDPmwBM1w6zxJXU4(EjXIql|xqbp#igD0V^}JAdQ@car$_ zxm=N3*jKt+o+-eF_Gl^hx9{@a@kwqw{z`SK?Ujlozg$*Ny2+1wyQv&@1Tk&%CcRu1 zOuDI?c6U(U0fp1A@V`^ahaH(r+raPC_qA@{Dat#bZ2DF8cS`-Rql0N%_y6ZMk*k0O zb)lmA@n{Ub1P8^W2#IaYfsAuvrpKoiC|(L&)Op$2F9*x@DCL+^Z#d`8m;|h zG2}nA|1D-8kIe4;ZOt2I0X{rPM(SR{x}+hx6I4D-^<1l-BGFkHsgtIUk*bfJsLzSt zqeSCt_{&!i&R3Q3IMcImkW+s0M%<^P^qZjKxoA!u1gBm+*13G)SbJ$(o;nxJgqh3u z_))nWls4P=Udfd%e{2}3GsPCegvlS~k>XTR`=tobe=n%Zhj2>o|T(HTJ&Rv*>A-q^kq}1o$9vRW0tR&a3pv`{a^=y#^3~ zKC(zgUi1*rLT*Lc)$y7c zkk{*cy`tSllzI1+5p;s(5C`(*uuFEF01DwsqnQ^|GZW~gc6=ShotT|(%P9>;o4QPw zCeJGR%$L8O8=k;@R=fUQU9u5I-#u+tv;#YZf#g0zr!FPuHa}-SVB+;g4sgj08YH0X z^jZ0iVV(HmFFmXVyMfr}Lo z7Y&}LvkQvQ(!uYIr z1;A_mmtnrQ(-Lz6Eq|njITY>nSEAfV`)h9i!=odgoINC}p{SVX6L)%OIj^U`cetH?h}e56Q5Y;HhgnPr@Yno;b+o`E9y(6ptpVHtWM% zKuJx_@_T5d)c-_yM6=05vA+HKqWyQH-cYY-43d-~)WgfqdavAZpS!x@-t~YGzL14D zPe;UB%lvSy2rh!EWtw@5=WrmX2gPQxn6iw2#VLK%Cdu7K@@iNReEXu3j>z0)3UP@!izW9lWINZ0xR~%nu;=Oyb!K zOtZvfiBc$wd-b}Nud~tNP@ucV1x+DmZ)@3n@S-L3K`i7gIIeI=^Uss-?8_6>u@H%c zX_^NAMgETNnOmkh6>Ez+tJQ{Qn+7^J*&u!orK5omg|s!pN_6uZDP+^4RiNT(h%T)F z1TJj75Lu(bOk4OxA^#rZ+Y&Yn>L|w2Nc_k(aof<=^-j9&g_45^FKt(VF%K|HvZ_e?;c9l=(x`S$EMdECm;UBbjW-|nro0hK z-Xv7oObb3oqmg>zsyDq%O-`x8J6)38-%Pae={hfw?`!XfZNeXdGF3E)+u$y6G-EK< z^NrbB{Tt?qESDyMJk*Uxx7ulB;?bWs9anez!F9Drl?A6C0u>8!^${Ae-?|ZjyP&oS zrEU%TJlsK5?7$pj`=cJ;c@zFv3~M%K$=@~@L^qys6!DK5%)01KJaODl2oj#>Qm{K@ z!?7jDU@cCzw<+s=D)t=Psg{n_eRI6P8K^#U^gwy2zU%dKN7?TJMVP=29r%F7S~B%P z|L@^l)PZYBoLPtGxn)>};Sl-gf#0FYFF`_Q5zpiF#slo8{rucgJyE8>%IGphZ0VIo zd5GP9S-nmy;iD82e*3W>QAfU=OozLXz2SrGW-3=I&2knPM{tX<`Hm4N)4b-3#n;HH zqA_Jq1cJ|#D|LU7n2%3ZkVzy)_r(n`yXfcX2I~Bob|D0RK}YKoJQRd0d}haXl8%Gl zYeoaV8qFA&sUeIj(W1LK7^;&IU{IeMbM%lg4@k9}%+4bk+owg4U1Y($BQ%#VKMibNoyIibMZ_mmtq{Iw9!$xMLDyoPuC)9y3HXrdZsg!cLI+dXGI8TUyj7x0y!0 zh6|ZTul!~%%a{1gXbz&XFe`&2VwH$!V(%#kk(pHL(P_vxTZo-BU$6N7A=7nopT zKqocEegHzmnG1KiUXC{vTx*F$Jt1X zBPJ(^L9tWMY@5z*B^(i8!IRGr>J#18k4EB^>yB65=qSK|Fa|R8(=ymonArL!3QNo;W~eWMihs#m z#^l%53PS3{%B+qCx6Zxp9sU^3262TQTJ;0pt~K2^qvm{1s=uVLXY#Tstzqkyz|UAt&nG^dZtX!_~6@waiFgr9dV zM}6TV0>fY$hxtKX#dar^I9QMUdUy{cP$#u>rUdZ>;EV^*#)sBCJe{S#b|V(ILSJrJ zhvDG|c}x_zOEy5BRq`KJ{ap4?63FLkI@zVcgP+x@cnDPIp08yj#Zhhq3VMG=mQ`L9 z?33e0(N6AA2kvh$nyLk)(AkY-dE1F=yVDe)2eugNf@c7ISj5stOkEwSb)k}VSJ}ZY zx}BRk`?RXbsQPG0Pq%*WgG1A){5 z+_{fGle3(1Ul*!n8K!~fDX0}ogP>12o0(m(h}R#unWn~)vBF*1=tXZVccKMr@s5{= z#@UZVO!GQEuRlt|^%nSJ6$1ET?E^Ff%{W2`G_Cn!UDnEE$Nw5Cr3^0NuzeIT%{8jJ znu*skbNlM?E-I-?_l6d3(Ty_coDz;w9ofs!ngz`Apn+%e3i49}cH(oi+>bRZu$EAd zH-bS6=SWAZ?)~II8vx~dL+lN2f*0QMZyf&RN8xWR|9g%_ZKJcx7@+Gl>kP13reo|q zBSTmF_I-nQ(;Syxv1HoxyMxDsA%iUA*w)Ux2rg%=1{=wCtk}YBg<3yCAGV2am2kiC zSK@4mx-*Zn7-PPVppSNaYN_^lqvONaYmnLaruu}@xsn9yeLvc94m?6M3)=xd&lBC> zazX$4ypD#ByY9|Ck03}tm*RQ`ZsYSa`THT|04`I=_cL-;s5xM2$^;j7e?&s7;3VW- zTyan6?JSA;Zq?H4Yk&EiEB99MIv-+!(Y#I$de1;rB2M}ru~(aP2bZek#By5{yI06N zVuj#~=MqvpK-2dNdHUcL#=l1@j;tIYejVT5FA|JtV%XAkRTLRM7Zj_f|BKO&o&Ce` zOEpW;guM?EiwmXSo9u0b5t)Rh<+T33G|WlNQi?*SqLu+tU!&ind95b8h{VeElU2hr zLij$6pHb6JX$L=K0%;=Fn!0^!sFaNnYq&ORc6I{V1>)a=*{l(J+s7>^6*C)&MjiGE z)^C}A5c|R!-wk}LB(Y903mwaeV`HXurf*$N2|z8OIPo7L#>SRKam^?KAAmNic-tYBp8Ad zDG&G{PP7li%aja;@4v%8d||ydWj*JUeq$peqou2y;Qp;f5LcoSs7NM^FI=q5tK|yR zBOa^z+$*w=p>}8WR!RSMETEub!9Bq)su8sXz=mWfaPyWHL9p7Axqdmem$#1HP@*DI zG^lkK;?33_ej|sCzNSRGFD1R<^14adm9V0F3AD>4aW_2SiN)g7J11&9S zQ}TJ-%&L&ozxh=-4{&t=t$i-H-vJm>rR+m?il$y5XUCmbdj<|1fHE9TOk}ED+xuc9TR+iP(d3sCHnzFD~E~mt*+Ur4~h)6!* zp=Psi6AgPpRynspX;>eR;6Z%^(zEL#sHr&CPYpAeDgWj}ro|7XvBj6=#W?`!6=$sW zbPy|aJ`e~X+D^(7@w7gfA>3d~TSseS%iPEi@<|(%IK=oSQ|tq#b2OG~FdZUR&b*%_ z#}>N}Y_p-gZocGKa>+RBbyoVX-_qem2)C%ER7RvDawQIN9| zq{y3wu3Zytk2`|)T>09>CTXe~)g0h&Jxx8fFt1x#iEmBEGKAFYDDd?X!Eqc2dU?e{ z&p|z%d`*sKZh=zKF7>A|i`veUXDhD{Gc3Y$LN>#Vik$L=-2A{aSP+3=ll`?2f5C`g zedZE~+ysh+uo3U?7#!~RBrUhT<{p zq`1>19G`zUOY}V#LwIdhBW*;nA>71e#Z0$ZdW#@`gp2G6Lh`IcbaV&U`oXS1zaQJW@c{Y2{^rJBtVtaLaox`!8TuJ*u5)l-k zyl4Y@_@5t`)D93wpJD0B5k?)sC4 z?*9VT#eCtDQw@!=o60qcn1_`TRtF#NUeoo>oI2mr&fQTTcfWX30bTG0+R$|!g@w#? zrULp*J)}FIXBo}yV}r4G2>m6Jj$>Luz7LH9N30-K^z&AyVZ-KbdLdidA7^xN0WO{d zm0A1h(4R8-9(a;_G%311&O+GFeB`u1Ot)H?bhoIqYs9@%ngB9@aFM}E?gOh6JK4_| zUL?`2V4@0TqhH&6r&y1O(T4m>+8onu*)vg}e>$k=m4|E{`Y0^;xw*(QQNysJBF4MD z$lzqMu)GM58Emp%(7a(NF}l-xhehlQlM@u>tc`jJ^mfqtQZlgwN7(@R5#zFysF&qe zVbSWH)G7+BEv>I@7AOyQpUD_6Uot`>MSepeOhJCiB=GH6qmO3sw}I(M-N@tD=AC>j z_4q&Zyp0`}=wf94Hu}-UVlY1ci-BwQ*kZKcx4Rwa(0lhfm*q;IZDXha&ol-%9#r|M z@G88b?C@8$)Te{mDQx8`_VYElkJssLcTEOe^0!Yl`rVC)>R^ADeucSPdd`fnY!$)& zqI|KpDm!q<-dAPN5pQLDIsaw(R*^_c>k^2WOQf%= zm8NtNgsGP;9ep7BL2QRb_i~wM=cKYz)z}P(noEE*5pQy{Po@OR9X1;X@0G;DVqx=! z{F)YaLq(5Vr#joIycF|SW!A+pl*LBpvZ6hoXrWb!ZuSqBu5u%LU^YPq7g!CM7C|Oy zUUofwCp*`Ro|tIrtCMFKihU2kYdeSHT;b3iA7nj0>G!+HZkg*ic*;v}XHmr?Rpn(} zkVzI+zd(X}0+dwnQFUprSWAObhx_}O?Ed@oBM*x;f1uR0>t2>5R!?Rx);pG6P(CiR zV24%?y)s4PUOdT=Q-iMC+UTs7^^{!aDrIUZ_1+CII~Z`c(vvr(;;^NDNvr9lez?&Y z9)F5JSkSY&^jw8^X`{8-tSTrazYE&vjCV=YsGf@ zg)dIpHEnQ&>=9lbIz0>rd3^e zs=#gMBh?>lt8}$rn8AMiWz;d`q0q6uVjBPGVo!BwL7jHZFAlXEh(_p$^ zygI0Hr-eY9mx@>=rh-rYD=3*jRxM=?udq7-^jrF^Gui{Ok|;SsB}K zT1>FvjgEoVyp!bx^WY*X!tx8bMop$b1tQqFIqY)BPx5{k5XLmJrz||TxuD-lj`QBW z9ZhM{b`}8tybWtM(ULym1-PU2`{>Xmd}PH^VnyidsTJ%J*(1*4YN!6cPOp2zi18E6 z{IJWqucO+b!F`o_l$1$cZw^7Uy@5OvB7-#4a3*UbQ;?JXPCkw5T3Vw)9df7* znR051RoK5lwmd5N^h^SSc9z?><+FEJ?aLIj!>F!p*55STsKC&=*r%^6*%ipWUX7B2 z#a-(Az6F}j*Dh?rW*(3v%|@laqkwdx71r_#i+~-04r>>Jq*E8qw0=4kU0nj(6YWq~ zTb(!G0IDjQTY(>vTBbD(F<2yTOngGGKXbJYuqez>b0GkEQX@aTNey8mVcCUujZg~*lH5bBys}T?l8sjk$hgy;3#Ti7Y9?CyMyr>J3XeTG@W-0Iz87n z* zM(Z`aT7%lU_HtD{ljuN7q=9=98}W+n%Eo0?GTZK6P$01h#9|a3byew^tycWSkF$2% z+e1#&-SfC8IMhEG+G2*YJpC-zx1K<?fk>} z{U2|xnMGoSUHeBRE70r2=HRZ>a{Y}{ocBbru)%#Q2E;x;f*_twE8QrtbPQ-?u4})D zVFvg_4J6oq#FT^bpDWLvgbHqY z9ly9rc5gYdnv_hN6kKP%u^G6*VXjuK8yhCP{35X37*|C{7G+s zL2SV+1toQwZ{ zax==)rvo|T<9NDt*nloIbOJ-JXQQh<3v&}TJwmVL!N-g`(EC8sL33Xr4NJu*dzliK z<;}L2{KSwtKf$(8g6y)6G5}qfDCfz9m6)(Ox+shVox98&%UZTXSqr@17Ba=vW5KeK zIw`LC%MFccd%tl2P7VZCw^@f0kGE0Z_xBI~wYLTE*Ha~yuX4bGQS8n;5d8ZLP06FA zfWxCfOMf)lwMIl#8{-aJ%jA44e%+9Q04~EJDaov?$WDAvL?mCs$1ofhYU&0W>)b zXEjE!cr5N{Q(QX!5~2NFOm96)=p@-z(lp)$OOA^!46V zCbQ4HSDU_|sj2$#Y^P0Yn8^3AJ}rpfjYh)f9m+g3=3IoNN(N0m4Ol^8HcfD1ukQ_Z zuhO?r_z8QE5R~1o1DQs3>$pU)4z6CdK-!Tb%ZwP%tt{An2Ep#L60-eJxXu1ybFN;E z80X9+=&BmkxLR}Go4DAPmtPttDGfI!;Q)#zAG+1FA@T6hR24egA5KCe9LfBb9`~bd z5#bqyb|M+J?p121?&T3=n~(4fhHp6~*6^t`Y+6!gpM|Jjy62Zzy+suYHsTs=qW9N5 zo}ge(JA^MERT6BW6Y}2~=kj_aO+Xitq0iRgLNqq(3MZ?$nl4q-zEH45)4vadRIjay z4&E#Kz1UVD0K9z%BB1v? z0EPEw53Koq1s$-_C)TVp-0?~&_ye?HDG8xQ4@OGN(B4MX9PmTox-9s?#Lq0S`ImE} zA(D9Sw1?Xt8NPWq>!gDiwn{gJkb`=fU5wVTCputB3t0)?|6PcoGJLXAmZ2V>Dy1Oj3Rt94K~Qs_$BcVo^sgM-kTr-O_R)6aMIg zhq15N-KAQErTz59yLXih1L}ct#C^c;!I_Iaws;rE_h7W=^uMmmdP;2X(NbI&wo@u$ z@};$Nhn=OK`s9;W+L?M%Bt}m4%o5jou-t3IWSGSogjwauchDHql5$gRNCsM7+`r~l zcA+#Gt(U^lZg*WfDj${8?XC7zZ!H3U;Vx4NS!qlb(hVClmnnZL&8qZlvF+~LEIiU~ zL+9RRvmrjoodCBi*r%HACeu7zM2rLPuu=I}Ny`Uy4mpEKa`>?JfcB8<;1uU`)~@M| zjQCiGQ=KDv zLd1i#**pwJHVufO0O6tTv_7m(>4*c}^ZQk2X2fGn{5C5?EHL>#;g~vF@ALLe0S4q8 zw-dO&`{P{{mcpc0nbikl!OJ9SLGn2>_yuF%kc3vBg>(w5sXdP)V%HAuTGmpHd?f_S zA|%d9ArFqDYO2f0;2xU?hs^BQ#tpt(!z^w!pgWa}@3SzJQ=OcRjGn~2tW!cp0^iCi zzWiU5+Y4_9g`3w>;K!6xL(dwHoCS-sxCKsm>QVaaAH)}AkKx11+KvP@Kshoj#)Zw> zkwIeD6fH_o3!6D2fYAk#+d32%ESjB7K0)q8avnODG`rg~!8uS|T+%j_kro3IQ|@qJ zVd1cB9y*5#ofAHyd_?-UduNh~EjnMD zv|D9lCH$?^`3H>RvnncGJB9X)c1tK!bOHO-?SCdbN?POG{6BUw(Ud7Za+|fj0QuW! z8Tc@=s`QYh)+a6_Wxha%$Q8Wben1DKrT*P@T~^=x z;*yb{pwDi5|xQ6-T!IYqoyR z$er~Ghp=YDJke+5Xve^a&HWNf(FiDjh5ET$!AiISmohQ< zYK&aAK+&L>D4xT{Tul589)2a0j?#TBiNP%Gza-XD&{Qg4i@)OKF6=(p_Q(iAh?97R zdE_vOctfJ3#XC>|jI$mL$hI`U-ls2SJdMMK!5z&t! zJzkxL1>80WEcmErYoLmK7;(m=U-C{B?b)UwIpAp3J{J+2U^6p18FM6@ef_NN`F$s` z*+DI;fmRJGN05H)?t|cPBbP&YCJe*=@xk;~9*}MlKzRvgc-Rk~k|4A?M3t~#PkRD3 zI@T$YSy!}%V}XFgn@cl~u$C*jXfdqBiU7+3H_>)G^manlo=x^JBJE=)hV6E%hPzFr zOr7ps(ngiZk}VgI{mv!u@za7wkgKQUX}aRK#4g0V2k#(9iXd7XH*YzX&svwS^9_D1 zT>=z7k_uwvpE1{(=@H*>M3^X=# z%!a|1uM@_~3LqMJT%h%HzPonef6HxZEOD(fQ$Q?nT;nQztdr)cj>61Y8Y1#dncNC= z5Wir#t$F1`Ceuw8oFt=d*;aDEZ_`UVkL_T_G}Xc?ujnGFmWI0<_#i4`sdQuThrj|L}N^>SRtwd{zDXqD{Ic0u#6y2D&gJFEa=S1%Vbhz6`-YUBln z^Ba+p(n!(YwdSVBTAUKt-P{t)y(`JxJ0Ke0R|7)sm1o}0zMW>k|9#X3u}Xll9k70#}iU#ofnOz>=UbFJ0^}k+Ent^U>aN zl8i9#Ns{-K4|c|Y+=i2KH@*m9R+Z~cr`yru} zi(C&XIDEfx1LdD2i0e5gvGie~vymiyIt8(Tp&xH#MDJ(K z1ZU1H)%C`lg|*X}pS|rL&S>-uQlcN8J9zpCGy?m7G0FT``)#r<+2xLhDFY>L$-Uc@ z?vru!h*g~EBFa$D3P0}=pZ4Ro2AO`jRMgllY$+?X^tJA%P3r%4%zA%Qt@De9>An0w zT*Pp^%H8`@y3*w*q`}c9JtID1|HaxSpnZ zVIk3wYa5h#MZ5aD6*jXugWL>`!v_dIFlR*|>o7n#Ghby)9-GhpK0GP6$6v5pIMoi*07K4 zD(NWUZkaBFg)!D&$6A$T8PDdWS;Er8V!zt0?ciG9*L*6@1-v5VI$4_2q7I{a5(lSW zZg@ioL$T``i6{u%+g6;HxH~eW6cH-&139dZpIfUIYj z(-lvF zreS}}5XQxfVTEAlcjZIfrTRdI!$EOhlo)8~7-If&x1Qj|kY`(?@4nG54lu#DRRE?$ z@DKV#-^eBvykCNht`l`>O?>4k8PGBR3gs^djh%wjW%3&te-6ys0jR#E@TM3#1fts< zs^lY~3Ul=PA|&y?YeWpb?uFPs#3d`HQtyQK}AH0exEbf#1rzQ zOTW-Xx2+wr*Cd74hWlw{@Oj+Ojp>dZKhCpiiOjxwx*fmBq zpO;lKD5Tg*tXBwi4G#|(Nukf4k7e_B>v5DaYH2k{YV?%e$~LjK9nYaPt7Ak?yy(7! z*=V;q->bH;*4n0tj1r~taQu`A6ZLymoFf3cx@_x1^74|W3%`6^h}uoW6ud|@uMFVx zvAM&x%9Q8%_fqsACJ|}gMfeg8A&Jpt&wmC)CfV!6Wn9f-XwK_e(;Dchjc4tgG0P2w zjqa04%pNRnAhm4Lr=%tSKYxdjOWx9_C*w^yr=G>1Yq-~^R`lw#-f#m|D)58oqIz#e9Y^j=7c0^H|C(MRm+i!mD!Jh0-g@LER>Fbx5T2my9!UT-q z(++Tcp>=u+xBtYf4Q9dZprI!QW^mC`L4He`&MVmy^4(j>on%aYOy-Q?OI(he_our#f$X{9INW1U^MlcL` zw(WTQ6iw@hie+=L{X9zbdlo%Rf87U@5!B0>5Q$}5Rm%aF*gG2AsS&du)zG;}-LS*_ zzgtGWRB}Uex=;z3p2)ekb7e3CV?ncs-?F8BJ&jl5{QYtC$cp9Rqsd}eWB4g`?a=vao}Ha*>n`#92w4QAI;<>I zflq@X$54U_Fi8)@7%OV>zi{fcT;G4;xbG$~1~#gS1>aH=5MxB(Y9`=t(F`hWr|-zK z)m-4>5gnEsfJ&jXesH&p5|rq#I#UZ!oghyYt%N&bWI5g$&;>>Z3}* z3jyr}e=5J21rEl=QMZqZ+yx{g$QN+&)Zy7pih6U2l?5_R%+1#ZKz97^#w z(Q~p6GnZOt^+UDRYc;l_2p+bQKJZ}Fkqd1H0jT`?z|hPd-1l@OV6pIfIv6@oESOO) zJm!W4)f$oy;x*o6ODS-mq_;@^Kh&oRTIey6Og6OT!6`769}_#n>F0XJQ;>_0w=X7W zK#gFfyPo$J zB9tOQJil#nUrdEiT+}s0G?Xz4*ooc&Ekmx49r{A2Lf-#*rQm!Yo^LKK7)j1FrlsE| zsz26Yyg}-tiC+|V(CX;MG7;uslN*xCxLcp53l%??esmA~SyVU@`?JGhD^(?I!G15U zW0L=$rF~h;z;DFHHf(&eoOgueQ+uLDsM2GyEa)^fs=u?7tBVwYO6M}K1v3ac_hwM& z|EQzS44+EYYi~YqfYmQ|6S+||BcHvw9CmeEC-$7pK|-YfxZ8Jq%A14fC|kKa*Gs+u1~gQ|N=Y}a)yTw^@qOUD9%i!gPL z)D;zW;p%z2YD*c&;kram*XJr`)?d=q89s2tLUoyx05-=2frb8Y<-f}O7c39Th~I&R z6?&XLV?ObxHinwL^gcFhadX)<&ulJvc8$TPrd+~RPVFHGy;|^MM5pZm?33s$`MTPc9YWwc7Dlr zU(0atUkBZ{^aoFN=kwPsq1o?8{2y;UAGfK^?;CSp)oq`Lv7|aLGY;#NbX7E8o9k!s z-PC0J^gUl6S5fx7)AvtrG6*_jX?(3Y$4)4I`kfyi-$RtX3GICK>v=y|hD*TsdYITA z2KM@u(tU>b`rTaxM)~=n>X3^y*Re)d9_W-`$!)ANXbKoRO>{1fgP%{260L4H$zvY*u1b3pSav@?Q z;$L@%^8b6^7xFw(1z30UAX!nEd6j1niyOO3Q*4$$%dXoMsoQ<0xGyRcZ;0#4)QP$B z^H=+!udjz0UA`WPZ?E>{_1uv2*J;=4PiY@SpbNPo&E>n_4(o%f?>;9Leq*=3@%+6n%?{n!I z<);)25vYw(sWdJkb@p2#(OfS5y`T5}THQpr24lwnnQ$Z9 z*(15%eAw3?_MHv?8Qt2Gorm~qL)=6`oJ(RHkB2g(YcH3Qntwgg=vD*xz5FW?`sY9Y zV-c2L{@U(aO7Xw8C#h3QTQau2zOM65`0Mdpit}H8|NoW$|NVzw{`#-~UjF~TU;cXL zytq)A;J=;`%ca%(3xECh|ML&O{Iz?1qdyJ(`Jg`~g}ZYJX!-3ATpIghDVzPVGE1NQW?*vs-VP0u zA{n;|NlGk=Fj4@=rlVEntyxRc z0ik%%|JNI}tjn$BWv?aruL}M^^b&L#!5@Ah^&VDt=GRg>sJSMo`1IHLNEhW6{`v3n zTM~PjJNi%0TFOtU3L+eP70d#ae-UOC$dBuFX8{oTk0e-en{T&P{<6C_@#kj#+{~Yw z`g1dXZsyNT{VBiS+Vk%9q-*EQ=qKfizs}?Q&9dUxhW|_@v;O)kMf}fH5bxx7X;sVr z^_Q1l*4-~JyYhFp&l*yk;U1RI*bn>KhH_@-u)9r_6{XxpJENwh7CE=mZ(ZrLE0qq4 zpxnk6p*>Wg{gzKNI-`U6z&49>Cb_@`)BrBo5!dGHwLzH^$F`2EFlK(1H?WfpCg3N{TaWyH78%gA>uLZoZvWj`(35T21)SBIdl8koa^tD0vmPA zm2w+LdPFy3UyK9R{cReRXL(aH)f89hxD6?^EE;*lXaSn*krLD6; zDYsGSCJM@J41Ha@l>!_0_OP`hWU^J)2`A0gu)Egju9Vwo#wbPi3?+p*COObnPM@H%Djkd*>@q=CnWypP)K;+f>g< zzV>x82&3lv-G12FFLmlF`jUp?)0;`13;zAMngp&Iw}z{7yh3IFoXIZzq;tSXO~`$l=@7s9=s zE*qz(OY(fWc$O=wvV33K=qsx10?I>pY3%KKHmz-=kC$=R)qzsZ?L2nJ-6kWHw6F?H z1xF}wrJG+jju01a6jZxGl)G^~)J?Z@U}tS-xVMd5SfcH`R9KiO(j<@XFnzD{akxh&lC0xz zg?6|^VO+9MqVX=1VR*lscZWKsjC5OdIUbYYR9Dn7HCwZ zj$JatdSF)|?XH*vwLrA-xn$dnN~p>hLFgNd?F&+udkwC<1qBJi1V+f$`4yRKoJ)2b zlg-2jy5VFc>+FoDT>RZwWdTiGJlZEW+p!qAVQecIqvKEkKhs+c#aKWQGveo8HX zFkyb~r~ExL)}R!`2T0feb&o6I?o9Q>)2M92qAh3>ae8tn-Sh#{z6_|lJ3&HopiHWw z##xZ)niTfnge~GskZOszao~h~-`d00B)`fR(4Npr&TrM}HodT6ggaCK_6BV_qpyU< zpTumHcX&m-v@Yxy5Z|?JH`s+?G75u-{q9_F7iHfic|^0AwmRfGsaLdLIX^`Yn+Tyf{q4-JncY!1Gz!@*dsb2=w?6*%F;U+>JqRJL)VH7dCgA=BAFar(<6bvX-A<>ThrK{Qshom+)f^~ zQ*O3M`38>y3`Dd~DMY8zYT4TPONH57?Y@A_d=-lm_R{U8sitQ}d3JaE9kzbmK8)-r z2FYXX434DQiS(SYAhky;j@KEhE?!e6h=ErUs~=u9rkK0#z0~E~D#e7<&8JbG5Ei&g zr9mX9u<@gwduo*t$K(;|QdgCrF%-*0!d%T^~cW-Cxvo>M@AESbr5OQ~oe3SsTT!*5x7wm$4;rO4L3JyaR(@4%j( zIu)?OU{Ihd87)k|?@PMDP@9QpRtSa`4Kq|)N5HET||FL z0qXWK|8IeD>zfMFrwY=$f=2->1U8g9Qjp>d9Ge|$XXB+2woUv0|1P3WnH6b&(GMw) zUy%7*Sik^bU(^qucM6ip++7=xa?ua9`^j}IP5rRg)4Oc8=%BA3)?8Sbh?1)6jYvAl zUr5gaAUz5B6?DViSACvrLRUgVjX#so3D5zpm=O&r?T6i=-92%HD-14@C}a6fPcOH- zJjFIH?3`t&$im(@sz4``WdJyA7Lfc}(2K~NIV&zB4sjnukd>VQTMvFvR+r=8A=y{K zc}dEz7l~XI&>>BiswsAwUJv=kkulnukPH){VXkH#!wl{8fDfiB2D3VdGgi%E{;_fT z2fDp@)t5$Fh%l~tB_`0ct-3->R6e&?ymkfDFnuoI$&A zvXPm?#_>aP{qV6d|2TW&B+V?-AJE)0!p5}_wKE^+%Ll~aD;4%dw7UJ+gvM^Bkq_s@AmTv2SWvAGk3PGZ>hS?FjAbq7%0x?YCUttjmiCd z(Ul-KzTiU8oM&=#ta8k_m#Ckn&BXoubGzp4oLd@XQa$T2QWHC8wb0LUgj6KRx zVZwP~zC5XfCD%}`7r|{xN0npPTc!JzialIYbJ^sZa$#p<0ZpIKl)Y3~&0~HCNX)8+ zPM8=t%uh|%#cz2rIOD1(Lsj2dcyf>FRS*+7P01-1APz~*6H(p-R`T7BI;a>59K zrdr}zu1frC(``;wIA?AsHfI!WTuL4FYvcViozWO-FNAU%8}np(zt$V&&Y7L>{HOfu zKJpHglNx%3DXhl1W4p&R4_FtS3M-&xLb3KeMIL-#f zn07a=m8Dj8u@)Uu8MXPW=d$;Q|3*p{Qqsn!5Y9Y1WBDl{In~^uw!eOF9$hPwE_yHKH-8jihBRE2z>nrCL5YCm zB$yJDiAYL;k&v5W2}M#wZ(51S<@3Ym-9@I2VPPlP2@}~CHbusT&DZbk>-W@g8RJRb z-m3Z~jGJHh_1?U3ZFQKY3T&y-Iu%?6Ly4@(;n0G~#0tdzkTo|V>oCwR!rFCJy zfT%0k4!bZ+MqzNTvg`uI?$X|Nv2r3f0?}7Thl~mv^S;UvVZegAZ{N?iWVreImcQPn z>5OUm*j;s>(B;@E6w13cfzh=U+;5y`Dx9}97z-b+!7!R~m1f4$mH-STgqYDZpDrZs z{pz=ogP6dLt{&YeWzKeKKqGibwXpLJfa+mSm)%9Uwl3`0CQGr{EX12jrMEJ%3F+8} zdt-7zGCVX1KI@$W)m? z(NVtGP7`}Ic>P`yz0|%FhT0<8H}lZnn!IqIjiA{_`m3NJDGZRj(ag2=62WY1a2COf)jepAx{mao` z>-bkotU|c`tXI_-b38rlV+4F#y%GRqxbXg3JMj*KZhkcs$BTF)H4iXJc3I-$0!UAcM7Z$V1fL-FH@8TdYnqOPF}w+dpr9S zlY=Eyq~#)78C1?eTp|;fDi0UP!sUS#dwE2+-&RwP7Akog6OVQ~?`YrBcW?Q)5EJ(C z+Yyx57~#IW`S}u(u?=g%5^mHmbR$%#rPw?!1)JtxAJI zzST*4!?YcCaEA7^KF{?U=ebC1D!UozruJ((%q4i6Mgu0&P-n`35g4Ed4XJhxl{Ys# zAi-c3!Rq@MEyf5f#^}w!7|p=w)&6}O^mh#UdmZ$53_86ab#JSdLrHf+Ft*G}p?-9v ze|jq?L~gQ#^DxkY#K|Qq)vhT`05&Ke6E+(kPq&B`?2bsiSh5s z>?`_J*IgxY%HJ}*3KutJemvv3dB!Ko`feDevY~5;)5-UZ9LYFtJPnY{#!*` z(ozwVwlI+Fie{>c$ZcNH|wrC79xx~zw+T3NEad8gl}ZHO>pv-=SRYz`5d9!OvDOTkKu)iZ|+sj$hEaEzG<%cvu2YP!a>;X_2|&JDouog znx#~#kIE(I1B|&6H6w&$#PT#odTn~!b+cEu#$FxOjWZswd|e-T8H}-e+dqz}ju8&S zjYmCh3NrG5_Vtt26qMy0-QBuWqm*^=VG5m@+!(fKnlfmC6kcMPs>pD7w7{R34ohA? zAsmkQ?o$1(R_P5o+hzTGhvUtHqBIDA;R_LY`%oD1rpW5|-)|VIf}%A2-Sg@1G;qNF zPlzyWs+DJ>z{Y@$sS?Rpmosgk=%iXYa5xX&+3=w8IPAK5{;iamdkP&~>4m4PP%_{U z83!td^!$_H*`2jRWrsLlb|7tcaohEv>gQOx)`5%`lVxlXnb6u)JxU|)ANS8Ga zbV2u82F8|khtM%cU|cKT$JIR_VbhTiW;0_?|EUQroPrTpk4oYuohK*^f*^F$*^{*z zARTrSe#hncpX_|{fv%H6ePE1nxu>>~aX6*P`?j}(+=n;x_-%yJN(dZ!^Jep4ear#x z*c1S^{kqMe+1GbfoE*ZvCSHqMv&3cqt`XZXSn z=rthRUghNdp>7fmP4>~IeMKC$>7txl@A~UlAL0Vm?Oo^V>#4S8q}#eRrVPmKC>2(w z?8a?EGb^w~tXk^>Kh=rluFIn*->;`q@n7WHo@mIDPq@(dk1X{YzHcTO=D!9O_#& z2fS2sfJ|gt?z2?HCOgP}<$|Cn$$9i;(b%Oua zbB+njcP6Zi_HoMbQ2M^V^Y6R;@F-2)RRnWd;$|ch2Sw32k5Ii8D2zoRKeEWoYkNCa zX=I#7B@})XIo?84m}*#mkL&9UQUcK%g!(H>o+ds`?v{|8WLa@kW5Q*LWEoIm0Qnx)FqUA@T|sN%rw{Rf;(S# z2?O>C19S?*7rnypQSAT=V_$sa;k6L%HcpWUxdA>~rx?uA)KiIM>=VovB92;{7|6=6 zd^jI%0#amOEdB`_-d!fibU9N|eYJ}Zz zZC<~dvVOrF2s!Xeu*2HQ7{0CBFkI3<=z2b6imm2bp;C;!8?M?PhG%&8)FTQ4mXrNz z)mHIoa<_!!q*U$F2YSxYJ6QJXe$9MBPz80LL`dIRddh#+m=XLip>s!g&xIv-`r9t{ zDH%o7=W2Oa2~DgSmxl@I@^BGh1UY{@_o1nVA)S{9AG? z8yd`@BlWynDJ>rb^t=Km68aY%<=o9~J)bl^%-3DgjCh!D;&-XGGjxbEzS^tU+)Ni^ z4*8h+g&P;Fr3yZcNc!)#n>e&c~5)b$4Jem^b$wFtQEUT7$^+~YX z>~d)IaW9eFg%0eu^#^Ty(eA)oLU}Z&RFp3vq5v|sw;2RuGA_ROQpCvgs>Q+vYZ;d6 zDzs5=zLAgp$K943kk9*s_9I;+WodWF6c#|61_1h%eCRSZ!j@weUh zx?F27lnXl>3zeydV5>8?aMPwo4$@p{hV24OkY=l`wNWeM=GQhBD|sC5lv^20Bc+Rl zz3i@)UO}VmyGwISJ4!-C4HXPEScv%iG|jku@NJ_h>08p2^sT?d2{-Xkm}G7V&!A5E zzEXcHM0}5*Z@;GZR_Y8f&fJIu9Co*j-CMF*=UlCP{9DpI{%vS#MO8+*jrI#GZDnW1 zV=D~ZXgQa9T36s;&ECv^>H<7t;hi_(m{Ta`>p9xZuyU>%O}zg9Z5~ojQt&yA>6K9H zNBY0pr!f{|Gxve#aWm{p?@bHzuTndGoW4(JMO$yKI)3NTEctx0%IUia24bO z1o?mqOa@_sR49*B-S{7OIZhEJQ(eam>~WWM*B$-zPLaHCF0uvWg+bhtW)wTu1=2%< zG3sJM^--~H)saLd7u&P>ke zUq1THj3X8TJac%g4%Ht0P%TLih!(p8Lhf=ZVC~fh+akwU``F&9o=h2c=@tVb0d?+O z-VUk!5?1>~Od(`!E?@etsjjL!m+aK4iGX!`^918QbxS$3^Vl7%eE34rNlJwAJ?9xK#Lz(ZcW~y8Ex`jXZW4;x^_cw*l*kZkKb5T8V9K~Bv?hvqTrZjm zW9d)wfF6%&x+#RY-R-7ybyf(|h-Jv4E)d2jO#F1oT)ieIulY{A;NSegjHoR{9Y;Y% zB(CA~rtn>%@LfOBP8MVTvfWM9m+fxyFG+@ED|kKR+`WQA?<390p;XJ`+NJw8k8xeh z2`K7}BWTT!q(Ideu<$%QQ_HB72fxXSB!zh(_?#Z@*1dA*9VGDE3derf$>4X3c`c z&tctNWKBh6SbtICPSK8ehkLIwx>2D(;aqsmK;xS)(XV}@dazlwE zjrO|OL4adF-ugp*<_cI3f-%;)I8pb~YLMF%C*3``YW*V6b!#$mi&Gu8d;w~hpxUj(uOV56d?_#Emk>vKrje+$;Lfkn#Y z@Dr50GKj+RS!uNt!WBCSTr`V6H)n;6!cgcLA(j4HNlOwaF$4dp4{;pf5C_Yc`@=3; z+JSU$%PGzxzt)bth8wSy8?WF-HmQeLgAK7>NIt2*MiE=Uu>EL53@o#mdX$6gZ;k$N#XG^R|4C%dhCEOVpx|Z2dZE+2YYqI!o$JZ{OH!~`{|B+I1 z-Br*nu5atZh5Z6z2lBhiP0&rS;Tri%3)NIe*Jdwlv#FXqcI> zFf{wLFlWZH^(pdfHX$Jks_GsyQ1J@(l2+tRL8GHdRh3XG(zQ9U?nx*ftL&DGU`dU; zcRCqBl*97X(CbCE5?V;7QzO5_f~djQ~r?Lz`rK-woc;-$C) z<7@~&Q@ozSgAHXf@(SMW%X?azdG_Eq@K+Xz}X;(lVJ4Zd^vk z(azlaN^83^VS5{t9)t4v%>(rx{U)&6_EiTgb^aZC2<(;6&=GT-k5ZI-2r*WW>Z{P#I+Eq>2h622#;0N+C? z!1t^J;5+C5_?+)Bs|hI*Spsj!guHNU5Q&F}O3lmqwfX6=4AjWn=mc_L+2+L*1h z=Uu4;|Iq&He=!)VIjk~1dn}ko^KDfmS||{07fT;)hXaeYyVZN#M)|k5%cg}55^Q~; z=o{&%o4*cqu?WI*VeIx^a=02P~QLHfHq}zN@yTx_6c08wJ?~LKxyI>2~$nV>=V}8_3aM(#I8f@&| z-YdR_Q=0glmeClt|;D6TFc`GOI2@ znf2f~Vr6%Rcw+288cc&dn$0f%G%1cP6sTj}lPTXiy*77$2jN7@c_A$hb`tUwn+c~w zbMdz-PCSw?Z9I#fBlPJaFW&!;f+%Qtjp@|*Dex05A@OZ)JF!g+9L^UA>07Y26DEq2 zyhsXO6b$96i8a$xbNHe6)=tegI*HZ%X72z#=}b4niFP>sjOVFY4{`O45q!j#4Isai zF!ZBZZ$9B>=c%bDohR7oEHX9~C5LzRD~0I8-Vr)M?tkbrLM+_z^F@Bvo$Xi%u#L$R zoMAuV0rnS$*Vb+*5n5u`fuU#S$KUzy7hN(X)jHrL|*1#DEK{tQfq@r@}Q9W-ur^(fsPCcf2CyW%lv)|O_>VJ-_+4NFC z+=mfVr4~_XNM#g6=~jpBhC#cHo7f2zHm2&G3!}ou>`|$fI?kKB1m|>@prGoapD^a$ z(B+~xbhas0A(ym!OP z7!4zC>ji8@+O8LP+DY&Q#Ry4Lhoq@O(&UgdDI`41`X^Z+`V~k|t@RR?J&V^1j=q$@ zIL)V!!ln5H;h0Yt+xgTjD1*V%KT(M$+{e1a80n!XQ@C`qSQ!frffzsthK^qdn0F}= z&PVsMY4uX0H$my$E`k8jNxs5~eMXuuBM%>%{EY!KJOL>c>yoFNUeo@LVyVFK3R*>e35$2NN zt!t>M@eGfs6?xR-`PPbG;J7n^*{Jh(J? zvyEVwEA^xkKcn|+a(%7ST!eGSu z$(dH0?6?5~($I-VI)d~=9ZK<$ERplsDCZze+C1zh>;a~dy7Ba!4;Bhjdvi|PGDPUv zLQt0G3ZjsCT^(7&Sj zTo-D1Vff5Nj4o+l3OuTnPbM@L4^nrS?Ph?T(qji}AA(})uQq^y-oBxk=wa*j(RXS+ml z_DdvZyF_x{TpKyVwUM)48}Zz%u7;f9(#M&en45BBnStmEhR=I=)av~Be9f`^e62$E z`RW1t%nuF`(b#5=0{50t%A>;kR3VKF2zzkmXuBTsy$Z_gqy1~MSt(JxI^)-I?GWEr6=w$ zyDMCT?80t%N%noYK_4rt-eT{mE&U}?T9D3|@Oknvz zgxf73P2z+IDQvt;^X=d-vzD zMY$@$##&)84C4&r-VkW>WU!R{GvemjR&ZBitp&hz^*1s!LBfhdPEuWY%*kA5kaQoUUQ2a+Nt++gA7~q=1uh= z-PW~?4mPcgkzp%!;{>78j(bRC0x`YT*H?@RcgJS9Z|o>p+wwL~aozp)Uz~&p5?o^f zTw}a-Z5npxZ3K*U_cm+8J**9rj79%vH8f+wor0;(3&U&U0>9uYVi(e^--Q#ex%%I7 z=SsNwyDJ=I4BytVz2UgBzcux_Am_pk4&GmsILp=30NODXh*#B&$|HNFV*VfmP1Dw^ zZAKEQ7pRKpdApeu_QO7>#}I?3emJCibWBvy9a6Qx-HCqKRJI}2v;ijN47OogV|$HI z{Q!n>?#PceBI?v zsn4Bgzpsj@aOTTYzm8xOx`ErKOt>N5RXyK8-{x7@B47UDU+JmY^RP>I>=|nq20yPs z;%a@O$1`)n{&~A9?U2sa-AvAQ5e5q_- zHnuNeJ9aP&0Ht9Eq#`JFNgC4f^1zP`pjSScAKsqX=UP%;tG$#<~hBm2Ftz z6;#*4!g}T4b7ipKI5h8rx-ErL6M=oBMhAZ!Bj(;@UVVVGcO{{;UZq3+7hlH ztg#iCy(u_m&xF9wKDjexcc$Iw^{Aj^1P)GpM~50*JttFfeC9m#ufCyAz!jE?*}8Ng zX*E+htqVCBi&0872;@eQp*gPen3I_JpJbX>PeLIP-{RC#@w$o9thzM-D?#YO4--<{1Sl>@pZ`?FB}C( zN-rt$%Q(^M(0^QS0kMJz5yOG{Cf+%2;+^p(9svu-THl*c%064GyA#U9_iq!f1u(`+ zK^QHkxu_7f#a+5%(5gf5hT~|$-l>p`zBD&{QC5iui2EghE}?fo4H`9IatFWjvhV=U zl`-h=eTdnf7v9y@;G^VL4`K-c-fqepDEd~AtGrN9;a5kc)Eo-U~-wi*<^*k2S6?S0$78am+S?e`yVbI3sy?F%%A;O&b?gq;ik16Ys zRwfd9+BOTqc~TQfZU4H&`7S{XIq)y;!&0l?VJ<1_QU+YHw=zlAO#|AS-V}oci@{Pg zJ4B>xThbdALYSvx`%!PXD`hsOd3V{IqH@x0ZQqTlpNAp5wE^Zc8qLRg$x;}%oc815 zM$LJ?;0!OW%@on6b}xilP2nPqo>~Mzxt?MenC!k`{*cH7LiRIms&I&-Do|Jiax@fOQ^ik7S;5E=HeKFQdmQ`V1ZFoF+}P zeSDY6hlTwPbB}weFxwsKU^n(GNt~$Oo+t*nRlA33K9VD__gy5xeTVNF6SO3`*Q)h| z@lb$D_JZ=6c_qU4d}{tohtlh%C*z`}GA5j190|d&E+F^jTTX_8v1|}_7n?GIs&A_R z>-Jh`-3j1PjeC0mRS68kst=BSHEf}8O_36d5WaRwO1y*s7yZu7 zpU@oOr;9kwDMS>UKC4G$!b91g^Ogh?YD%T+f(d@9m9kty?cIe>G-ul+PQ!~^y zl&kE_=A^)?;9PbakFr1Lz!zV4^=5}r?n2DlF1xYO>k~z6f5x@ai_95$Ja*aP(3(&b z<4bJu6ww1V!C#;buQcQ9L>DhQX9sS$i$yvz@s!c1vTN{$0{uAWdDvvZ042x?WhBX3_ zdJx9`-QmuaiiDnjk66nInk&ccSSd(%@7PYu=43k3U^C}$bBxn71l(RUCHECn&G%j8|B>2 z*y>N^fo}J`h!{$a2)-$8>Z(}QF#9$iiL%3|(hfv!cMWu4N$O048+nR)FoB*|nc2jj z8a6r1sggPKp}(EQU3T>DY&S3bC7A4hxINN_Xl^Y(vIi^VS>*kr_C#GpN?% z%n=YgXP{8>qOE|1)jNQ4>^JuYXUN?nm(`s&>)TpUFf!%bi%|(pJIby60l(1tL(UA ze%w>~{)oZqrFrhK5qWCd>;*85^z%yl7sa)v7ZYMvyo%}dTyF$PFB_wM? z6m@Qd22MZpXJ^0WlqvX8@!dq{jPVb`epqjeYK-aa*T2&mP^4uKQUo{j|1dxUpgl1^ zj#8gLavM(Y;ez?(3tJ|?yW9lbID7ZdUO)8LLPMwcK(>(46QQe#FfzP+oH5Yn9N4(0 z>9RC2A9zpGkOno~(=^zl+3fO9lX8%fD$vUZ-uJsue>ZLWT%av=IBZ%fTTS0q(-w+o zl+X5csrGV=awqG7N9^9Q2q)haN5I=x-x)Jm(?}3jMVxyxs3z#fLAdhJjfgn{mPSOd zp?GQQOL5A$`K@aB+8eG*64Z{ay`hR;V^u+RSQBV8QvOd4<+`rH z=(iba(B!ag6*PKFFNlAS^q^pd7o0jh2=wlH{r@gO$3eM>_ z`NFx#SBTV<*3lWe%NvKl9oN4tCNgk(yj5*J6DK!=4ZDWo%(-UcYPdm?<-s z=^wAYw(y`k==$5(Z>IwFrjHM7d=1H`PideZNA*jWeF}!c;xAu3>IVuR_U_;V7sVgG zWb}~demfPf(Yy)tM(aV%Alxxv93e=X&=22(ek85pj~+{MAEmeNl2#7E?nIY7x>Z(h zXmvbSrm4v0M*-j+fCsCwl_TK~RB0;!HX?}lDZdMyAKIbJqbo-|x-w+b7sCz89RU0Y z@6Br4#xL&3IQItS2VzLka=*t$uXebBaEDk>f$Z7iD6+COD4w6V^tc zs}CJ?apZtS_x!k9Yi$xiYGEnFY&yxi-S_^dKPuJj+DrX$3wvfAzf_eGoc9!B!Gt`) z;fHSLZP~91EehM`AD#1$HzB)Jrk+^!%W)KCJ|f!fx*>Ot%6Q6L{RQSCH}|ZE%_;Zw z+J4(8zycQR-RkYgAZuG555-jvFM2AnQ_Mv9R>B<~9wDJ6cm}N74OkQh>H8tGhUohR z#1L9AX8RDGe*trPV?&8B-WJ#a)t_{7VI2$q5QU#!g3v)@r$j_{O|e~dnyqDZP?fOQUu zu1I#9%lO?}-ee)H-@c_y5`yOb+w=D6ig-#r+Ba*Kk`I|v40oZpSq8|(Rd#_RZQ@96_wm%* zh$MxLBb+8K0M;=GStyQ|9>1yHMeDW?qjmT)ngEO;6XtlF=A?tT`eVaGlB?7eCcbuE zS0wj&6vs@2yg(}(Mbzb+B94>%PN`uM9F?tp;;M@muDbldRe_-PSP&>T>q^!7u6w?$ z*j*MwikQ`G3E{3Zr}0&W*nwaq**4M02(A{Fgs}jV3~Q=JMAat3b^#gWkX<2Q*I3aJ z^dpoC5_RwHbx)oLzYT<&W^sGT|j;uQ;2X3 zpJD6r3-evPFJHto(--W9mrW8}AOS8|aW47DXd&!)1&a{t%Q+no5+dA5J0~40)~+Eq zc5Ul62bKA54z+Zt`ye1IiXCWj=)O?oP`G*AI5m&xl|L=l<#VweL@z1hWCTOMyaPm< z!+ZS=a-%q=L&Y&&tt_HLT@ef{k|fuW?5pmrhwiOEH1%LWsjz_u$ryrB5A9V?&0iA| z-*!~F199eWPkGqX!+{P4y$e2eD#&jPvMCn8KJD)@e7GRf4i!6)-(7BkZk+X)R+-rI zY3djX^tJLqW&C@1I72`^b?%24cy8`2Dq{l(MHHMhu=_Q@2#mp|Kn#a6hIBQqila#r z90S|eAeZ!P7?W|>w!_PmUG*wq*W=VTsYfhm^NNCV5v_Sl%^eeAiqC=)wA$En$Ksnf zjQy)|LY=@Jof_bw91C~{wr1Rpud98D8<*q;6N|G5f9Jh9S=)tJMLXS3o{Q6_xGb2M zWoQYFNrApB_p1}9kkrv<-iIRsvINgabf4bpf98SYC!XS0gbf=rsC$hVi{R&ajF)G- z&*$N3@~ywLvAW(3o0Yi%mgeXsIvh3*l>)aeo~mOpR*nVY!iEwMUSoU*&6pTs5iz_r zE;eTu&$8@ofLFZ@U?7I`TZkVag8d%E_$~#$$E|mUso@{6@h9ih z;p_Hj$u*xWxyGu%wX}ENx7LmH4~BcK3c@+B0M{MeHQu;oq>k#ZhbA`Pt_NRpC?XWA zYyA@G71bGCo8i^dsJfbc^_$t(eAW6IcdM@-e(drJx1kF<(srWgA&f2WVD0;3eO^f! zFt6SNG0JST^yDJXS7Yc4Xb0&T!m{KFy6p{C& z4<)$T(#EOxq$tI?_>MOu@gr6gW<{ih7X=u^Jl-7i%|nvcWwW{fbZxIm9`WLT6&B{g z|HHltY$4DMf-z6%s9_kl0jl0-G30K;qPicMuw=S^jOJ!h}j?gOOzUc--HuuFz zLe*QmMnw}*h|gEd$nMjQwKBk=j1m#rZrQlLRE|@ljDvgUd^oQRx5q`k_2<6IE~dg) zf~SYEcXroUFt^)WuISj-tyf8BgPo%NHL2VC28PUf>G@i~_A3hDN}OewTU@&9ScPOWJPlznuy1F1%_})7jC-B7Wg$Z8-^na;laADu8?x7+jAygOee zQWWHfhph{lbzBy@*1GAgsF|*SrA|(z+{XD8b~^;dZb#tAe^6HRtKd~qpw=OlG>BcN z-EsTy7O-Y-&JE-oZR1=WL1aQxf6hz#SJ)N5)~5xocv9dx+X(2l*^|LDF z-+d&dVnVntTNQWCnVr{WcTLyR``4<}3*q)Rk;^fAU3IySB)ufY8AaF=Iu!4)?)si) z1K5!#VdRiF^;y2k&UTYr`@}kY|lQ`_Lb&jWQHdY58yMz1DG(A-V*uv??^Q-sndT`B*$zB6;tBo%;Z%FF{&QV2{ z^72n$QvsArS>_xBKX-FCJUzz1eenYt;m6G%%Qfg<-Uk2hR*waUTJ85$>y(I3le;A( zC!b<<*nO+p2BF;d;kC72uSLUaZi(s>h1Mj9)QVdzilGiw z1CCo9NM~_KR+m&iV(@W;HwVL|(H$BiX-0|im}~X;AwOD(M!SQ~qL!hNZ=k=3los)b zIY);yqQmEU*}^yv+qqi2Fe-xVC1?asdOC}D#cF93r2d$S>Ijcb6f`v(Z7jfs_tVx+ zZSF;D+#W^0`OAp6&C7^4SNc_?HLjrERE6=O{m0*x!IMVt9XD77VkW-h20p{+gkFR= zL!Q0o5ym@qJa#~wYvA6VQ-7U6OikN2BjKK_} z*qmXK*alHW%bniikRR5UKoFEz3rkDt~?EVXj5Wa`qfBBa~)1I7qgtT8! z?hw2OXuzNe9s+RvxU?YdmjrYL9f%GsRK{Q!fo@M=skH|)72yZke+qc&CqFE7KO$^D za>#z9aQ(ng{fICPmnt8IC>>2zmEuhDEtk%G-1yeyiUL%{|c6Hgd zZQHhOtIM{HF59+k^{#)dea>EMV!f(Q0VG;n;TOWo?;e#kS%tgYZKR&cX@XC0w^-9IYfxc~=* zCv-#SO2`Pob}jbY+Ajr9*3fn|Ce&$o3oxoylEecc4qn`IiC#zjqTbhPyY^@cs!6(MkSj9eyF-e2$GZX>&T(~SK)cWO5D#Mi4g_=MACZ<5jwrd z>aOktJh=aKW(PV+ai}(aVayOg2ZDySsvf5ul~Ns{D&uB&wJ<$d+Z?Z*4>jf-!U!Or zpR*X4G<7TfAu}1FG+3E`Lxz5X;SRsTV>k2D62(zO-^Z-LY%2+5!kx6>qdT>YNLLi?F80SK1(1jE=TR)QA} zOkP*z`FHVp0uttGQ_{rpR7|0P2Q>y)w;4{hUo7-O39*jA&L7778CGMC@8%9-#6vj2 z+BEOOM{mWaF?1?Vt{69PWU-e3C=P$?)d?}QcL+B=S(9;^H%){qCB!IlDwQB;(*D|< zeV{}}fGee841Fc~!%qdC&I+1j!`qMy=ojZQ*5TWr(a zvC}+n*6O$~c`orp5{MH9z$uIP@Xx0`sYk`rEQ_A2JEuu*Bs^L1vMckvTMlc1Gj1I&0s0o(MI0zcB7o`hp9l5_@ zkH0TsnlPpfIaJ7fbDX$thaK1D%tF`z?1bsoraQcp6njKpwJGBj5G>IT>c68~;rC&C zW;w#YcsfjQfya!r_%;;dY;%I=jg&b^gInt&*=aAkxA5i<7qP;Lm^ni-m|CSa5jYYJ z4Q&LP@Ff+}ChfU*IO4+e+Y!xa8|-~fcEePbK+ZuWWChKieq&{OF)UAWQ(V_Bt75fm zER7tZlw^nb#w?H3fft3ZnxU(^@d%eB;2>W+eqDDc=##J)SurizNmZJxPUTMg5o9zW zBKN1o+{=cWO!vy;q51xFkZI)&mM52ph2?~+7b3UD>!ZaU(qbO6-K_4$_i?X@~hu}dS0GXli(OJnaaJ)`m6o5jmx#L{YHC8VNxE_#c8^Mf(_ha?Kh;|Dw z4#A4ki5V2KYg7+X-tP&8w73r09ftGh=P%UDjQB|YO~=LL^hrO~06&?b5^_7xfV>76 zcolw}S%Mq66~k4tMGE@Ed#$urB65M^JWJ}7c`=c1r5bzuJ9pixa4>jcSOGOZ7?^(E zH;r$mrDu<17b)dueX=?g2(>zKSbHxwUOhL)N6cB6-Ep=5*C0aI)43$4#fj+}>4w8S z5Q67PB@&_x;pv4vJcKts&ks1C)%|~Qsux{wK$mCXe+hC%@%x|;Z4rQ6F_1_=-jO0F z@G-?nta}09j}IAOA6DSqD>(1vJyI9iZTvQmjt~(o6_ujX0Xd1Can(xWWL|lC>mQTP z*Ug%a2VJp96_N-_yP|*w>8yKenssh#K~n8i4^KL8+w~hc^IrZ+T2r#k3;+(Awi&KB zt%LfAH%B@&i_L|pK?JD2$?Id?P7W+nuSz}W+t!hOPE_qK$ty&vC7>f`r23IA*hmV+ zhRLQ5cF;H&BEj;4E)L<$G#UX;m-_jJYY7!WKxq!1ghy~Wnrr6Qwo;dFCsrNEDw zdk}17kExIdsD6W_!;nZ}JGKj^fe^Q}<^R-6joEA38UDlj-mqb34~iw+ab3bFbwH|6 zxK{~V6AXq0{{&+UEINg*(?a=OMDE@4;~p3=#$HjL<&SmY`6?huC7@F;d=C=(?+^p* z!q;zg_BF2?%Wrqry&C`AU}W?WY`)F@%YD6;95+f31)_?N2RYV@z7x{Y7Bb?wg%Gz`C(QXPMR?77A55*0-Fx&I zH4gJY9*}jF*(q%S842DaC*RNS4BBi$N|Z8|4-W2>afI#njk$mpsUGn*$9IX3d5CMMAQ&-tE`x9z2=?HmMkCA6D(cU- zWtxB3VP7T4reDHnK(h7Tl3qw2XWMiV#n~BstV1yX_G3{}kINdRtj3us>aKClYVAIZ z<|PHSt~z#b3b3h>oQFzlLAMFibLn7qZYzPz55D@5w$uJMEoCtZ;hZag+Bj$ z1WdpBVXjICfA>&<>d2l=9@EVfx-fcu$O{$Nz8g>2exoqBdWmP{lQ?`y{nnzx!6fy3 z(NoB;UsKBlC2r?t!)};i2Z_6 z{ zC2mzv7|!cnzS@~$xs>>78itqt3iUqnF4N7_kA|EKo;^AQ-Y&r68{b97&SQ&o>fADL zoBr{Tyu~*mQBwT0Q1MS%^xi|`48$CS+x^Fz$BFDgJ^##M0elnF-QI!&mTUDZMZfTs z8u@=GK1rnY&ju|6L!8`Z%Vz|3>oq<(@o({msiP@3RMbl<)^`%t@bZ@;7b~RT#jOOq zpk5%QXvbQy4Ul3oX*T*<)mP~x0QDYmJc=!SFg|kVo{@~oQc`zri3YgEe^1dipZI9M z69Ky0OX@BBVC_b=3q=_`F8@E$><=K4<>&PSiL|AqI14N+n2wnRIt_oMkTS{@H-ck- zPY-1DZY5DCZF#QjcMtm|CzOLzATYb;;@j9aY=Qaz|5q66cE}T2q5gQ(`kA3dGclLe zg*~rpoK_NLqsWN#M%iF6QggU~!_mFX4CQ}GYez7FXlBlIv)krDBKakU9)=9x74Rp@ z(K!D{hdK9NTsf&Z&VO?T1#J}No8a};3{eoS^^0#jGsP@VOkS|9Xy!Zj`W`ySh>FO2 zof3|r9E4{ur?rEUMT>xG19FuN9qxg~rK-Xr=M z?j#yZAMNCdnRmp3Y5Hk{dt9Gu@)dsuNQ~f@T}HTZwnHE5V4LBz+!_O$*pkScF<-n> z#aP#v3d~ND?-5bOTwwQu;>=lE`T~9X2^~Y;8u2ed+MgT?Ov5$HkBcsKch+JZ}9-+Aa=pimN3`g zC{ttPC5U8qO(pSc?5{l>Q&Q*W3HVDUm#LO4_+?P1+WRFfFoHj{7mhVs$m7=b$QnBc z4pPy`yizByyn?IxWrT8*#6}jBb=btrMFmXltL%CPQi%psG1BfFQb{)uXY!?kz2zFq z(Ph$A4rj8b2qtHfa1w~7+g~j>a=JtJ2HRngWt)DY8(PVoI}wBOLL(w$ zz%@#a-qf#vh1uanzuWeN*z!5ErWN{iHM<$Id*A93qtkzHVH8zk2;?d+WE@6lBtJcEBu z!B;bkuKc5`pST7$Nyc-}g+F~%4V?BqJS?+8h5?P$)$E%M5h;CI+?dVmreBwRyNk`b z>sVPEzvA*!9Mmq*d#W@&KHH8Gs8BQ83D}k8#pU&;ej$gRf=zWhw^}80Bks9tyE7<2 zlEL*4FE5$GxVJszZzay+POE?p3c@d)7bt%JjbAxSjwow;bZsi|N!Tf%ko+b$p#)dC zIe;?i!QkA63CQ+F|x`Cy%lYT22pY_F{H8uWAQtCAHzx$d-Plm z=hB3`NTi&&E2(CQEdU7-#+i)uT(f)3@-^x>X5Yu_MW%a=ap<(Gs(p@esEr%Cy%74w zr{3^Y4@Dfm)SkWdewEQk+R;H+{tumtFPJwHKMk>Ml8OQlB5>Xwa@!PZq)S>l@YpYe zMp$tfgj*?B7uzGt)za20M?r3sY@h0hUO@4u08iir{iO}|yz;G3!S)}r(-p{c3JSuX z>#~w*fL0g=h$870xzTCv(Q&g(+60y$TG5`(N&Qqgk5H83whhYrWd)FQbpM10q!}U1 zIo&h$R3$DXI&s%2ogJCEgW{bo$L0(WZw|*FyeZPoT`PwnZZ@3WlDv z+s1hgJH6scnL{{2z--%Mc|`Uq&&HH)qvaSl@WgF5E)gEc=tCMO(ZGEEFs4&Z2Iqk( z0qaAs=_UW=l#*_92ovE8$-fKBpj2TX`wW`t*KNhZn-Bm^XiTVuKn;YR%y=d;2<%_| z!tz`kQ@!;T$0kpK{^(*$Hi%5hE{?8U0Z&RXu-TEhDb9t~#*AK;`e$>1ghFx#E13j0 z5*PmrJB}Cl0tAo6(!Vn(m1SM_pK};T7Y@UqGGYL`(mNQ$S=#Zcs&sKTXmVG|ijLY8 z6gG%s`b#)o$?uN7zqoh3=v`Tik( zy7zZFQn3-<*JKBvj_Gi41mu7VLp?M(Q+VAHtrcF)XgZ99U5*G#PDw0P>bse@xIKC+ z3nar+<3BJm>GRoI;|~%OZ1ZJ&qW(by-w3fyaM`Y-sxR&n4a-=1j?Ql>9jnJ21bSf- z^#G=q#Qkw-UfnZd{YtDCsfbF0_@=BvtS~=)+1s+8UNKLOWHGZ&Vusk;s{^bcV;YpL z0CO$!(@|*DkT>hN;Ko4zBfP#rQFm+*(RKDnNpVP5A#Y8QIK~$S$s)Wrae1FRr~DDk zSv0CMoR=@-BYhe)zApD$%z&`TB*^1WTdt)+f2a>(>A89ArPXti4yztuggnD&c8^k@ z|E4}QuIv}7AuCl=Qxq-aTD~y?&giGk4mB3k#(=gYL7v-Z8dxqunv4EWnm9M99_OKO zCgfh>&msa`2t3z^IjtF?(TjBCaTc>BiyPEE-`lCp>wgIqfXdLXF$Jjns@*h{fE@oZWxX#Ta88DbD>FHj?+zAra(p!~a zlhtc?s@USR+2S;DYl)cGD&*)plly`>3AeMyu*b7H0tjSQ6O}B=vsV7lIDnV)5$-%1 z*V;8h?BavMjP5r95 zcz(jP(VqDVedQK|oWlGLwQRoI0hGbl&rk_XB>ncNxZNa+W)g+IA#Wg zD%KAluH#lY&@1Ah!bxG13?WES#3^{coJxO@2(Fj)!RE_TBS?@I$}riTr$)8NACV5W zuVL@Zcz7%ol6S$N#vL zlWnC+S2Kw>Im&wPWwVPyt4HY*0}c~-dY`y~Np@}CC37Trj0avaZ-(W<|H3Nkv~EhA zK0cW(#4MzK?9@SA7>q+ds{JcfTq^v=aj3>h0)`L#fAQ-PfhTY}om>iJp&I#aMjz^| ztFbPgW3%pZ6!nd=*EJgv=WQD&s{5mhRyoa{a2d1g;#h2n;Y?GH8qZfkh*{!Vr;gGI zw_-Y=GmC&TowS(MXs6(=;P9+wWqWg#hqEih=K#~~OL<0U%!9i|(?)S`u~fvck1z~H zvekFG{LsYFp+Sf0EnRAI!BBg(Q#c08X_n_5uahq2T+waOT0@6}_b}7L_ma(lUEYj_P*~rHoAIYt2VrpeR(%4ZJCi3VH)HBZq=14TG&Tym(~@T+kK>n zYia5idin&W?IneEL$j9c1`6ulXib}ms($_D<1|JoP|v!W<)~S)wlb}@fDtEGTE&zU zz$Px+glMp9Xon6@~Wwre3}_>aN%c>8f^`%N^WZS&NY+OPa(Hu<6jS+n4% zU3NNMMM1X~TQxA~u9alU=~zI}qAz4x=}tgaPLm~ZLexW1DBDGh4Z|$H-4-~D;i?Ca zh2ysh_z@BEum?X$WC4}OQ1G42IC@k#1D>Uf%a1Iziwt^G`5wY^^Jy~_M#08B*imez zhuIn_+J2}m_rv*44;qK1$}G#KI&B_9*2Os5d4>LpJshq`Mw`xOI~f@SbU3&Z8fTQc zrBlbrqZ+h0+mCt`l++%z(5f0`lfo^mfP2vVK(ZR32yioM+)rydf^npOul3F!?O^AP zOt_Z)s;Oo)FsNAy;W}{*{R|%d`BK|8PZh@bGg75|p18zYS^YXMExXnFLc42htxxDO zH52D7E!|J2QKRRf!nTOey8-9-IeJ`D(4}bVfLM=rlVq)Wi6|}c2Xsr2BX$jm_dqHp zKJFdjSB4UzEyXf9vjI5`884Z`>V>pwy|fsH=l)_5nq#)G}6Y3kM2dmi59va z8g8|O!ZOpgp$R=CivSOpX0z8GbvMCAY1 zcjD%CvbqAV(^zUe(H{9-dQ9g`-eRkygAh~KSWvv(DEht1vypU)IxS80i8Q;|6J9Kd zBG&h}GFUREiAr3|z&o04Sx`<%gB!(lY*^C}1FQNyEGj)LS>6_edYQ@eGSg{fCsIlG zB@%D*hu#(pdYReuQ*3&fGCeFV|8?Mo_nsQEo6F%!vYwFcA>n!KgDnvcWE^6;@`96z z?^%K_btTh3m<%~4{25dbsX=t9!4%mhJMD*N9N=az73iuk#_d@!MTxwkBAm zHBf=iI^puA)xwepB{b^bvMn%U`7i@4S@}ve#4Fg)F)-{G#O`6l?zK{zcv71=xh+lL zKyy@9xaxqO2Ij@oWgl@(13VtkW$ZSPqcmfv zqXnMYZ4p%}lvYn1jq=-)>&cthjGfBqPlTLSE1O8|nT*`&e^3xYS|5&SVv{u55QFVS5zf}3v|>b`j@F!i+L+tg-xomofRE2cYV}RcUU36eCtqLq^&Y(CGK{bBKR0D7$(I##6L-y6 zm{7Z`;N!k2LKmw-+ojXpTB}wSpl=+|gcK1<#I>D#p^p03y8H~4TlN^g&!LkC-Z}Tn zH5GF71yYPJlp;UvTP7@nlkaL9zP*3!eU!sBJbvp-wb|4MdU91F_;HNfkma6v2hn@Q zE0Q)+)`40DN=i3o6bHv?$69%6l!2g9qXsWFMp6=Wp=r+God)E7FHv7ych`X|`^0XR zcd9qkTqm-KCzdLjl#RCuZdq7XzG!!WrJVucf>PO=?1Xp7HGGM9xQxsr1;>vG$3aS) ztT}um%zPfCSQ`Xno36gh(hS&zQg6_VBetxF8&F!A#YETwA7p9o57){NfsH&;Zs^A- zX6|4D^IWlgV$FZKUzIMm<=O(VeU5@FOBak5xOwuu+%G${voaXM@ZU614w8Wo+ag(b zkJTt5<2;nsV4v(eb#Dl?;Qx;N!eP`^CJw2ZccTdosTv(@iC!$g*Kz`hCx;kJ;(Eq< z+rAl<_0cUrjh@!W6wPNR!f^c4v%w-K`IhFs?i-orTZ&&BDb2gsoCe|-T3BA*8!)|5 zKC?87Hd@c?-kT_ACSV zBU|G>F<`gh%f5GGB-58igccXFi5%bHE4+Ws^n@BXUdFMNVtlO4tTo&`#AAC>_eW80}AKP@j2Au_d~{^4+_{h$-MO zrj!mo$_>p3EJ8zELZCp!$96l?)xkv`xh%gd1%h1&eVzz>H=}9*FCB@6eyLRTV+;6h zELZer`{{M0wr2DwXY>qY`30Wm2LFZn=#V0p4!K3?J%^9;`^59cn>woNiBZuGzpLGm z-Xol7maU{F1#Mko~-rp<<_o1A#8q!7-W@6fQ59%Ngq*NRP%2T{AD> z`z&vfAkfU%r$s~kzsRI1ORfm|;50XgWP-{;sFisUp46Jy(d;y6sLCtOD;Xpf_7gF` zmvx?<3RAOK<=je^T*eSHxgC$BjtBdi9_hhTkGA4P!b{WU)Z?lmjrb4g{O3 zfe7iCS$YgNd)U1(RG;+aF(ubRpLS_H+JO5Fi0u+yuZ@kg^_=K0E(r>T0k5<}6Pa*v zSNG`GGWSvZXmB8*8Pd! zJHgX+^tXzUl)>Ttevb3v@MG2Q_5iAeYl@vpNX1aazp$Ylba%v1EE`*U z5h>i%jaGt&yp>dLgwyXtr{P^i#9bcn zB@G&r#J9^D>$IqwDmENYzlh`Gp2i6UkpbP1U4zp%)9*YMX$Stuezf7-I`TL0hkb*_ zUaw76N<%;Enso6vu2{rqLtrL2ePr@hX@73eJdlo?-&+u%SPB@~VIq>jPhnCnWe{*n zKxaL?6H09k<7x~s_i?Xvh+jWI9HZXR?cyZplIWhgU{t1`Lgj8nn^Fo$67pwc?l|zZ;A?80w_HD5_nIvP3U1Ku3N*Y%7Dp z644aHQ^o_Va733|Xh7jtGSm_Gt~vZW*(kdQ4vFo zS-62cUx*MgAv6|LmxtBI?_iVQ@P?hVu_$}3` z{Ja}RNOL3o`{PvV{pHza>hFC^A>jS1C2+^CQtZlROV~z_U1gwcuy*G|3A+gU2sT+d z`EHY7DjxCNp!Tx(B9c~_<>{*6?4p~Tke33|SC*TT22J>RcBgWd2{eHB^zDqe8s}tr zym_&@b-NmN_h_4&An>f-pQVQOpk}n`+E?--7r|odE_xX+)_m(K7~^g6cCfw!U&g7W zf(mV*`4@A;B^@ywCqNFV({$B1S-v?ZPbCpMBRh6@4s)34FY+35{oV;?l@+-dnSDw*V&I!;{?ng}*ww z{v_J>%j>cD(2ZKwicrE%3}1gYgx8dG_68CV{f{Y+IIP3YUg5{ub}9x?f=7?uwes;u zJ7y*5j;1CopeLo1%sKx}ReiRu$_oGM3f6E5R|Slo5^k&IkT$cj9WzPAWH zpjYsKWxa$AwLWHg2A_EZh21oTS<*cgdEkC3{dr5(2RdPT>@P*5TGJ|O5L^vcX!zdW z50QBpDS-f&8*1gaykeDTXQ-K(-Gt$@==Tb92ljbfW~1sTCK|^riNRl8LGQ%T&QF*s zzY2EHp|(Istl8;hbdgO3qh^Metaj-0==fa2HX#@s5S6H_;;sn4>o;(_7^e9vW{d$Q zl&_}-tMB1LNFt$_%wR_bUk5+7Nj<*)IK7-Zaqs0MWrNA=?Yl}FEHl^l_ryjf+vyus z;pxXWJlK))%*U`2`onHeZo*K-o1`hat$d$g4nF65NUE0D{uA#)*K=iw%{LzB?~(*N9-r=k?+y3hoz-12hydtQWwocs$k_=`0W zPISjuIZhnqezn7o1hMB5g^XNHb$Ah2VQ8nk6JXtxdIM^2w)lyh_~cf}Lkx>PPIe7_ zbdPOMOmxK;IdA&F-A@f{Ss&X`4QT{ak(q+i$J7Mno5E&zXh^Kjn7w6@=Ura@tD2y$ z{6DJ+e;gVdYTSr=5Del9rySt_%hgMmqtROm3Lh+ZIQX?gTcVcZ)2J^X{$TgtF-qS8 z#0!VbuBFb6=fEyLIo!4V!0z?W;_%-~VY+yjl*zLC)iqU#O20sps&q*padb#Ja?5ye z0ow-oK($nnbf(qQ%YHY`%!m;RIP=|EBX%x~!N@}h+T3zbl>QXH_E0yykWbhKMeCJd zb64Bz{eGhBGSttc(ub(gFV22J3b<4GJqZyk3gp0!&UZKg{?0#l392oDF3fPXR^$LoCBxjDoGUcbME1|vb zWz&L;L=L2kp}E2NL&%GZI&p)Uk6qLTBQ{RuDnVGK_>_@r0gP+V(gp^*NKH(4B59t2 zM8t2#TkxPP!sdnrg=pbi0h;0k&qjGdPxWCcmZ9mUw501Ka24tvBG)7Bp}o_eUz{@9HlM3bF ze}M@5UD`h@{}bYLp8R)+lgaD3VhQ$UO{F6vC|rPq)xOY|I@G8c>F1erD{b;{X7aGc z&xf$PenJvQpCL~knf~*T*~zr8U{r}kJ(*y$+RXg9K+0#?D9S9FCU=Q&A6h8<#BuIs zFMrc1;>Tttbf#6|^NVPccDv6vvLUkcw8A~JBC^!-XDN&%K2>Xw`BJt)ms3)`t6Egz z(cu z4pqvKVD8W25jl0Jm#s^Y5K#>WlZ9^K%s1Qo<%Rrt4$>{U=O`hY#GA}}vm7w?gQT8& z0};#d2rmtfuGc8p=cB3JKsq_(J&ED{MKgmXh$0^1x8XH_*wK;CL|wjq^UM&ePpTnf zND)JAzIuk(N>3v~YvAswRM5C10^T7q8Q957(b!>CIfn0w+*(N5|;h`4On!xH+dTyA*Xv?Q#`EUkuoVN88o|DY1Z&0Nze~w*V8||Y+_U8;9 z)M(S2I}K>wMxnW%2ZI_+IP!Dn`z1*{lhxV-&X{KP2otdW=tQo#)=u{FQuGC)>|3hM=cPpUiKR`-FU;Gb+L7X~A-F z&d_ZGq~%Hm;`j2sZn?S_yQb$nobKJKIaG)bbdzake}O~S3ow=^>CZzneafR=Cj2<} z5zD;Xy7e@R`6fyrfpzw{Vs8I>06hE^U{#Z5kAR^O4R?r$O8hWR=qm|1=9V!wvY`Zy82q zWujR&;i}{Vl)HQOmI(EekYXCVlIR{=7l=*7=Q&M<^G~lmd{~7B4X)5e^ zcpE=06hqb2`oR2*fp_1T+blnE%jxE^)YBlwmUCPQdc)QO->rgj1V|g%g2INgJiE>o zRDDYZd=~lhopTp-<^gXf!LZQ?rj2>eE=h%}n0q!}f_ZZnFPuw05^4AtLjhy&!W0VH zxmKd8dg+}pmcQEdY!p`eV+CCw-Ku0w*1G1NhU~6|7Kns?E&BU0_tE0!XrUPX!W-|2 zA&ySPBalce3mFqlB;_5(#`jzwttUlsit*7U0?(T*PH6eV@l`zA;_+-li5VkDg!?Wn zmH{If@iyoODX_8$;lDiJGFovIIA_Y|)6_aB=EdP9e4lb7f?g^|N!u#4#D*w+R~CV$ z#9^TbbmpKtPnev0AG0St;bqcbCA7PFF-$00$5+WP?E-`BpdZ{hDJudoe~hEd)0IF6 z<=8}E7?d1u9e=$sL$o?W#A(NELf>oiFX?tS0OzD&!bquarXwtRRO;lH1HE~n$-358E^3mZdDaf!Hu!l#i6or3@GTX&SI-AB5ReJhh|0<{-F z4D^FTl9N4C^loxiHY^1Xh5p0Sj|5r}3YTlk9AMEE%~BrCqArR}K_Zi!aNKu5^Swn+ z#(E4ilwmxh%OJc~bZ3cw>P7JeJjni1RcaYKU$*SaXQcxjD;huMS3{VmaU`? zwYzBsK2^6_94nmsGM{Gpcdugj;m>x2sVTo>J(GC(iTv45it3G;N<`aXO{cD%eWlrW z#FF8ky^Dm|pyl)G*KcsFKq7?bscRfvcC=H`{OKTMjyux|p69qYr>yBUt|Zza%8j3Z z0jA<}?V`khU#fXjYY0xbe1dPD!-CJySqu!YdO$YFcTI3})}?$WQu5v5bT(B&Mta?( z!WS}@(>f+4zNuHa88Ft9@x13F_pa(lhht*!IH%+nG4TaQ+`)%;=D;?uCw}C&H40)y z;PSC0ui}0(IoWn^Qb{Zf2T9PGWK`cp|66rL zBbE#j9Rr-X4|1F0>qE))1uzhVeKT=5GcsmXclrO_?f6Y(cW_&Bh#?+f z8Q`iFWim9+##pu874+H~7htnW+dNc{>lx5&H7-ZXN?`4BjP| z8|19_UcR;4S``CW_^W9Fo@Gs7&If_qBm>m$ux4#zOD#zeF14<_m*hXTDJF?` z3rp?ffU@k@$aXeVxD_4oO|r_#tA?=zrr^x@&6Q@zJAA!qIt%4(7WKJLlzj2ewWATC zG=y^s;43C%e$FddkF5x3i`B^0=0^xpoNq)XpG!8h>x#R;W=sLsNHOaq>GZnPyNx6q~nv8c_6X3qd} zj+du1JP8347Ws8g6oejBaVEM9hL(NqJSCxhdP|JEjtL`uqXuy6z!Qs3cAR3rmm-82 zM5tLu+e)pE{`MT_&iHzaEn*#ZJ!(w7!ApO6Ed31id!Eqx4ed&|o%DUa_Tn!|94qcI zB{Ju<=0#SV+Bz(JjNnT0|GmfT4NLPF{Rts@T*@2B@c(+eUHPFKZtKk(FWXNBKeG^a zUrQ6R(h?`&V;x4H2#t8(n{CND(LD(wpj>^AqsT9;hi_AVgJGS_ATd4P)wyP)3 zU_QTV7t|wtCTQJ|A7sVKAH2=%A@6ayt$#>l09uK$ zg^cr5H%v8H%(-e_#bQ^A%Fz*bf-?tS$Nb-q|13^VLk(~e!6)eO&&iSSdgC=R8^p)U zJz}&TV2m;~>g@Jt7Bn975|T?LrB9odj?&v*4wBtwjO#*L=>Eidpg*#e7lw2ZEd zd}vb+{&~}XkM*;81U?e(ab40mtMVQ{KtlbUFmUr32G(P2){3H868&=i7d*xJ=d(dm z;AD!tAw|?wfgIhv-0<>(A}l9|h(A1$@;|2Fm@1L;$*TGt*^uqv48MQVekJ7mm!ZlP zSf$Hb>75&`)}(99cZ8B92!%@Lgj6L6e-zI1VM-7xW-J7b0Bfg!`z%)&t=sT99D$`lxz~e08amUrUw6BB5btU>5*i-Sh9%(xn ze(slq`=)QC952M<)+7h<#$K@^BSH`B+NFC@@gz5{y-GPxPZmU7Tg0#SS=$;&{*09U z1jo?Z1z zxe1l9_ZQmChfR1)<8AH*XStHW!LseibVisyVNUkz`Gc|9D!!@j*m`5Fz`dkcB=r}rB2USrv9L=n-x8dm1JS{JG!3n-1eUUM-&_Q zDKesLaIXh72Zo=++W+pTJ7ZhNwm!o~jTd%Xr1#d5S8Zg4Z8f%(WN#2X73vvl%$hz3 zw(_?ep_;d@;r2G1(yVT*3p_l-S!u=^p?I5jY^g))P=1sZVaE2rxHH*rY&>^aPIR_R z3bB`dF%!7fPN@k29S&Y|_$avfBV;L+Q;gDU!*5?be>}tf%YpaO!!4KmIj4&v27{hz zG#!;{*kerNp1KpB)vZJEJg4A-=C8neJW<2m$uQp}*srEd_Lg8~?v*X8GvOY4I)MKb zeFN)i9s87C2=ZH9Cs`KL7%ax6&)G|)Z!a2I3pX^J3k(H1iNLQ-f{qwHzoP85_7saV zwQh=g+txQKk05xj!B-PmQqa>|lsp}Yn0!+6pHrA6C~sZ_Y_;T^(z+Fv1ly$&CV+cg zJ*ITKR$TcgoY-bfJ8!%k#ojFM98X}<;@sQ1tuBQKm)Fh7+pMZHv0c(WacKQq04592 z(pAele>z6eGPJf{C{6VImUfq_x8y_nUA)Y)H!?0DU~Fqr){Dq(qO@s>HX?I60S zO7E*u^|#J;vAH!O3s``86%2u5ld+DZ*uWRIN@SxAeF`KlU{M&g+xdbOXN8!rr}dgu zes@lNKRu>QpR-~mln6=5yVbL=A;eQ+eYt*U5 z$)>>IWC2cn(vGuU*v#(mN?$LoIao$!B~(%>AJtZnqRgnkgu7pGK5 zY?&R%$Pht0+VhYm%WCM3C!301Ipd2xu4@{tYRBdq);}z08^MfFS1W$pUUg6)Z0nYd z1m`*|JMyoCqxb4^NLtyz>N{#JBb$p#Qq2m|_dcuZ=mA1m4*c~kUv5iy#a;`yE->W{ zrgJv@jR2hwgqS(hesSiO4w0x%E zE3p>uog=KZSoEkK#9w1GmXy&klqAG~%U>^}SAzn-GifI|xD#DYXC7Q*@ru`on^y|K za&#w5vgebZ$#u23UP&8>%E{E_Cjb{wZdaP4=XNFK4{4yK2^) zXOPs1ACwFW_`Z35LkM6>R0-;q8`>}FmfAyEJD_Hm7qLgH3&2Oc-;xjME>{J)RKRk5 z8wy3jC*x@(;>bC4fe|u?n& zg-UiNpd6b!jVh@|&LLqGg(C8GAuIe75937J7$rF47&V}P5BgRPDGU?rXL=>Xy3c}s z2=ph#c+rifrZ1>BDjRk_)$2{;9P(>$@2g%fu)E5c0>wjT8RmuK`br6cBHk}8kWGd+ zk&*@DUo2a-HHHZOOhyfuD`N^6aS)83ojjD|mL9ho{yf%OyTs=L!Ri(R1fov%V)M1< zzd6J$cD61q{HB0mY?+d)#2@LVHgI3p%wU>B{Nn1tvrl5Jm$LD=Hve$01@mRr`H0`v z+eo%{n{B9I;Do=5_M+l^9x~L8PwOL(&V}eK&`0TbiH6Yehdjst0d^~bYR{c&G^Fk; zu1cj))6^R3{51Ksk!c0~1-84d9YYgRXFgTqtajKTQ;rEHpA-Z1&$CmNbMm4-@BU=+ z_fQA}WSU)y+eGWB!>M~hucb?V(5U@|T)h@8Ew+Ku%tqiC0R$NADjQ#%Z zLEPFOQ4hj~py07^n9wC4m((yGl6gRUzx`4udEob5dZ*R%J&f-F_z;RL2cs$w1%d(C zyAhbz0n-D(jNeMd-Ur{b=bf^d_+_1GdgXJ8xYYPK&waD)Hy`-u&saS*y7>rd20g;R zKcc!`SeqYGv4Kh3tALObo&ez&Zw{Uvx~KUDh17AOX@6iQeQ_NHK2ncxp~Nr%IwO)J zAEMGHFK%K!QZh*5B{GKB3(N2B4=9&4ujFZvW)^zqz9CV+iAsLW*GfSL*R{zp*$h^@ z4%f}9I0%cDn$9VhU?)>EZym{j66cYAo-OEvp z(f*dLPx}&O>s)WHC5~Ww%s-O{BAno>KOKBnxMU@Y-)qX$BTxwZS@Ig9{q~>zG0*4W_YneK*#LRV87d{(hf%(@|K zEB^j^Cm%NP!w^H~t%J>%Sa9Q@FK=teZxl=1u*!r6Ll63O!qXIZAk3&xwi#mI(M=T0 zVolmY=^Dc;O||wM8PE-3S}AmOi8LJ=ODMn)L4LV@3%0hvXo&SSHy)Q;AT$$`CwKdp z(HI#nUY3x@Bl!fa-Xun?ZsuE`|4f_F-a9sMSj80K9O5MvxWZS_V|3XXG_+5oJYSk- zPOwmQMS?EerYYt&{=zBq?o%wv!gsjNecs>4 z{gd}~JxO2%wUDrIrR!z+IG#=JC)JsuYuLfFQEsN~m6H0+U(z^G{UzBXa;;IiVE4Vp zU&CJNy*{O0nnUwI%DjQ3dPEQG0dh35g{L+OF2qk{aIE)v;sJ21=b zA86_|QaF}C=e<@np9xo-LR;Dt00Re)7OzSYq}lrjj6*@H* zPaKj^)3W9i`8P+L_7tQ2 z;2$l#kke_c7+oS{JI-V04KaP`-6!X2ye#_rR#$VnG!#{k z|HIZf1cw%GZ8|5mePY|TZQHhO+jdTD+qP}ncG9`G|LW?l>Z&!^-)Il^WPSU^vmKCd zTCxCf%1nX!gQwJUZL(xD?4?8mx;HeWJM3W|^GHJ}llB(3`|PFtkJ&w@Bi~=}Aqt^4 z0l&{HnLTZ`A6#p`FaFI(P)!7|3)sWgifs&#-KI`eLW4e^FrN{{f?bY`<5u6^9}+H`PAoR|GN_ceUz-jO?R--1L! z(wXsYDhLiq1IYu0s}7=44L<1Bf$ybl?*8yhP_OP@-0mE#miX<145Bw$SdvksgR*4lnVk6_9e|F?+KpJl9aPHsPJw zqVr8FT|Fp2KVmE^0Av6~8Ufd8D}pSgh}}^ysSR3z>+gfoOh1Q_jM|jfd7r^ot|tfV zF0k4`&5q#Hv$@;2*JLoT$H_o{lEH2y3|mH2u2F51gk_AcFxN2@y?4q3`D@rblgM}y zLj?TaPXXWH$AZyxCvB!e%by>;Zsv0KMwL1f{aGYRdD^moi}~qfx=O2Obe)P9&+VJq zdiFjQKC@emP{V`$r|24=(uIL!9wdZgi)}5h&~WPj$61wA2hfLIr4L?2a?E*U{n{hm zJIjHa$(+uM`}Ot2qxT(p1mKkH(caMtLH%LDG+wxKTsPZ=*-o@;6p6|Y{!v2RsNzkv zog*lP&I!-GK(Lp@n@VG-ecf$u{l-1TLGjI^2WK4cxD~^OjU<1%l{aJ{TBrqW}R@{c3sBJCm@E9l|Gg{Q< z%f0g28MMUs&V_Ia0cXH3%E@CR!YV&?h4IMsq~GVy|4;tZNqDV33yL+NkPtE3#W>MG z6Du4RVU*JyRqFc;L}LK@k1o2XXEoO|XX-8-)cnOV&2vu8V6x99d;3-C&hGXw;hv0J z0W-ZZ@9zYsFPAhs3m%dDZDRJUA|Jvs$XGcEzi*sQpl3=iWJ<4u29|86h_kCftq6`% zMQ4oKlvUkpwXRjLDK9>hZ-D^H%SqrfCLV6WccVu%SbuS6Fq-XKzdPVR7j9>@+&I@e z-acpdQut`WzPa&m%`(M6jHO~J(W(dEm{tWV7lnnU8&URj z&nqc~ev2LL)$TVEx9b0S?SSNTU=;+=B*to3QBxGDYaRGr3%rq9)fJ*6Bu6-L3JKef zX3EZCab(cAF*4=0y<`#$G0-SXAe7dG-rvyUFXq6^q%b_SV`B|COA&R#kbf%wAtU!4 zu2)ztUY7c$~T`SIEDL%b7hDDc^5GBE#(W9q?oDGzqX*0Q!DqxpMBP|Ho1R zw?j{g!|84wPTY!KsJ1Sa(8W&(aFT5mn*|?F+H$<^h z0}m%cxH^|o;xitLSN-iTJH#uEz)P!GW)j-UnBOz*_c2PY0bf^0_2t0r+iFvACeI=r zrOTbLbI`o>18cES#>?-a*~RGahZHX3%D+;$P@`>(_}4M&lYzNZ*2yBDj^5ic1p);o zTj^jHUWfmD=Lto`F)w0{@f)Pn<_?l%q0r@_lV1iIDM^H#YzUk{*wl=HdqrQc6BleE z7kTZ$WKr#nJ_@5ABAChjnjUDtGF0Nji>@jm#=Imj#K`I>zscbo6$4BOBU&LWWS8a# ze@`{M?t$n~U*Zl@su**H_Revi!tfdcOgSrg>t z)YQzept*PNy;sq%0mg*0Cz0m{&+%1F?dkV0Fd&yf-E9pHj}5awq%y%2+0omJ9w3)9 z>`bwG2SmowP?J#Xwb#f`6AX}bxSJ~`(gJ(z-4`* zNHk9oHxw>$Ga5ELuZ$1P%XsaykIDbsF#ZTx~Hc} z;Xq^JQkD&+?snEF%DHq5fFOKU3!yTC);%6eXE4zr@gC`HA3~_t8R)FR)*s%&JCuQ6ZKt!=O6vu&SSd$=I9OB{mRE}& zli`C=1`Q<_8Dd>w`FrqJBVfQr0Y+b_9(xA<^B~v9-JFDnLEDFA;#jd=e?!WcL`YT7 zc-#h@^lG?Iac^o-%Z7Pj4ofT>Ax;Z{;!1_>1Y26)W_Q&3VA%R%(E0$7>I6+JgMCvN zFga0Z$p#WEMu&9{`;0BPZ33@@(Sg z2$F8VW_$6j75RQ5S=XG}6SABg0Z%W+_8<^$;v-^T?g(qhJXhq){RTUO1c^wAiC)^y zxOjmJH{mHvEA8WJBvr|O5}iOaklenTxe1Q5*4SNGN@xueG#lz}3cdA84#nSrknE&J z0Z>BSG2d;~e>5Cz;ijBDMS#tpMoMX~f~z90;mY1XSi*dm1u(4lf?udVxXgtft6FDC=+}{;WzLJJk__kjcFW+`swNL(ldnX>rVy7Pj1)nU- z05>bK=Ch~J9i3r= ziiuj=KXRnwo2QYR*QX9Ck2yA3kLWttdHSt@!bep6?p71$E2+~JoDv?GTb0f)0ee872OPV>m7rxhjur$wvYPOa2N3o_{T{PA2TAc4>7#LLN*2szubdf4m9?|AW39DtZz<$MS96koD~JD^zRn zguShw9MM^M7ftqDxY!@%p$ZbYDGGDw3yg_ErhAKb2V6dBz&Z-ZLqngBF%B2L2j33D zgiFBPb>W|fOokt1Ux{5kCPwf!g2D6s$Mw1z2>Tb*>;}`oH|&bAulr81*PV59T{}z~ zjG?9<^L)mbqRmrEReCA{1a8o8F%3OU`!hOh;N-V^{_#7ym>+8o4a38mxU!oe$Z4n# z0>2R~eK1swcut3>I2)JeyVlVbSMDyhL(nP?zV%-?@^}Halh0zwl=D_c@0w5PeS%%x zC52wwKu(fgS}ep~=FCW^py%V&+QD@xDOfD$B8B7RUk6c3S21+84^|e8@Fp`5lvos$ z=EsfI`@aXeTdXIq{!E&1^HHJSQ1MZQwOX@vDq_f4lgFjOCR_Pylb19avE*i7g{=B0_I zVUHMkM1|lLgMPBwIA7KhQPz%dngya}UajypF%F-C1#I_waBu&kysxsYp`Y+n zu}2iFU?>IKZ!Q&~K%HAukWd|SHjU6Ois3dOo;|%bj}RmH>j4LQ%~%$ct+EyAqKEt@ z0hG!JD{>QLzO2psnLWK%tZaPnET2QJI?(vat77dvf)4K}09p=#M@Y-e;iE$2?0MWO z$j9GAy)8sA#`ujwV#3Q0*%X8y+0`jC>#06;&`OE1qW!-9w76>nM6O zb3BvavnYTIXU_KjBmZNauNh{47NgS>`^aUb<6H^N#3!QjjW&^L^pe3@3WY_=rIWEZ zkGMFiEfp_>u;D~}EQT2)%q@c8D?p?{p+#eM{&206q#JVV8EG08o9fdurwUi-^SFvl zMTnb>Du7SKi{oyqP|Hcbyc2zgO`Iny1LOIMYwP(oLFk5(L*N|wMR8|DiR&J%1O5~} zNJb7!Kgp)_+b!|BI&vx_&(m{h1e^uYwr}V4=&4B7>+q0h85ibdyne(QSM=`zme8n7 z4+`_o=%Q%uJAZ(UV^kQL`c7PA65O;&aQ_y){5!pHJ+7d7TfWhE%!Xh9HP_i=IUiks z%yGOd8MsOX8{^AI!w9A6rR_kT-l=0vd=oJ-shJJB!JfVVBwVDt8=FDKhY~*_b_5^o z#LqOBsIz&<)(6`rb?-^!AHvNm5EFG^LeuI(`mJJXkRhg6fS~ueg?lAg4FrHYd(Yn8 zF`z>q0n-n1kd_ycpLHLNnTt2?p#_zosN9{z5G>+Zl)LY`;5F}owklJjBZ>1)b^2ie<<#;sfNm;!bH){#e`9aHycVSg8OnN zJj)gHICph=74~69*ga1u`0ZoqHUBWS)}@$zZjR8lG&m42Rk)d_>X&_+V-tg}H75m* z2J9;WKHsMVpVY&^`4@eCl#=mzt+h&v=RzZTr`h#cr30W!>u-tN$2`7!9&God(5gYd z=xISqp+Ws5X-VS|P*cWLt%j#c7SA?v{$)fLzUnk5F3K)J=Acr6n8mt0Nu)e2ldgkI zcuLSZS_Oq#q}95t)V}OiwT|yZ-=DUw8$3pCNA{crESSk&=>{fkUpU`}yb5*G#~#7W zR)907LHS3znQly2>fPdSdmt=(IV`qaz^n0Tub>hZ*V+1YlyZP}L{@cu++hYYGSZyK zoJh?xaAp>6#v=!}_7wx9QEvwm3nTemiz>nN6<5pXQXkbhf)qu>e*6Q5im6Rd zh*{$;bYoXKO3onXh;pHns{If#q~f`Yx6qE-lqaRhL?Veq@DfkZOAgcjosbb~^dVC< z>Eo3f@jp%UHXjevf`rS*zLVhExjh#H_OTD`ml4^@5?Ss>n4$Ejv>eUq=pFpfs zAem3NdGdmJ9q&M-RDwKXu8*DqRO<9Pm(x(;X_FJ)G42e6nK>|$$_{9>%@gaz@Rb~I zVL`cG7z7=oV11YQJD(xgh7%^l?;0{ z)NRB|f;^jaf0A9Gd~n+)p>R zX&e&`B*i<=ucRL~Qu;Vo1;rlHf)K3OF3#ZDjGUxYZ_vyo8KZB!vK=o-a~vXfhbqwOiwY<;A^(3=Y6z6AfeT$m7|U?bC_RC>X|P zw?7m10FlVRgOL*dX!um!*PZ%9;&Ts>3QtmW(cj8?`9Q7Usq!2Yr>_S8sNye72P?9O z(EW0`YnfK5V%OT(4#nUJJg03QOSb;>i`d;Yp4!6gXzE{9A5!b+Cp5@f%;51!su%x{y)G-Mu?=%bNx z=H|arZb4cX%v{7E7a2Isg0RH}6H@6Z0AL-FB;%$!P_Lk;`Uq&Dp-aK#wC<Y}0n7KjF(QswxD=iQ*s~{S z;xZ;DVAoXT@W!W3H+`*zXOHw_Rg2X=A=H=yNI>A;FV3A#CeF`VVw#2PKp5EPS=iU* zQ4d%0K1dK@=FTk)k8*XkkWtLs%6bh77h?5`KorR31&5U=t3$3r!+u~RLrck}`+9${ zdEueA1Rh{TMdCgg<0mZw;cpZWFFXKt07hlUB?UrL(aA|c6lu4?LWz)4O68~|WEXi4 zdAb1q*kw&L=ItItyv|pDE<)M?2(IS`TVW!GOM|=A*=c+v1Ta~4GPg#;TY*CMV z>1kx8>HVF(O$8RWZ}!j0PZ0pTE~5Esgms8#&ruHzv=M_BPtUD$8r)wRwAcsLqk~Rz z$dj7!{x%wDrpGEKb+gnYjS5k zopm?zy!#t0Xr!fFdXfZd6;j7@n39M^gG#BNoGs;B_pA@pX?8c}vytAuXl%{z0_2MI zkO=-7om|KIOdbt&WDq2hB$nqJ!wQU!%OnJAz11Fv!{xeW$d}PyeRs7MW}yTWITkyQ zV~W}@NT~25Dy~D%+aQg=x)2FKd9-*&^ZcTH(+V~|7HdYSH8xZi*bVHcX0K@H%vIN~ zpzhlHCr*Uyq&SpSp9uDQC{rzD`YmvpK;qK%X7&pH5fd6@@r;$zqhv6B#*?r;{z+C<&34L@TLIwuyVImB*Tx}g-wkBEr zn3WBWUKZ#0ybJ;IWN$sP^GNUFB$tW9EkYae^QXe&Qo zF=rK`6aXLpcJ3F+n{+cV^A4HHia@n@!Y@rfg)8THFwN3zpS<4Cin3zy4W+JLZUNYc z?K*lG*#?U~Yvsmw$@EpVwc~l_*?e0q;YrEiqlrN3}H6 zdx0O(MWs?ww7Z9uChPW!u3{g8lN*g6v?SFyAZ`T=N*VHWQ?PF9OjV1!r0FL%o!?ks3{z;AE?HihRrlg*# z$7!hxljIa9Op;4mzRgnmniECaQedO6S)e2nQdI)PjlZlbp+EUq1CfqW#6`k#`Du2c z!D3}Sp}{5=bJE3et>xFjHPOXAsk`?3g00gO(QpArGxx|q5S{%_p90eJW|}|N*Wke; zZGIKhL(MLG>ylgiY$rQU185)EVwqD6#uU2Sm7HxVhP4-E3^kl%7ft>qF4i?U983~j zU~qKkIb)|2-b!-)++q2FKgt&9b^MMO#p;ft21oU?FAgMG5s-7ygkktqUTF(Zw@E`)=KtqEmFGy)*+y$$?x z+DR9>oz}VMoA8EQ>7o*mO0c&r35=f2;4_gp*%}^vq`wEX%~R&kI2$e1i zGSx&Ftz0ecN-6{~2}SFAan9u_epr3Lx%D}bj`L+u3ewAu!<$JntFO`NJgQnr2^EZEy3V&<_erJO&qk#|+ zES1i594%Q?&pHD#5Y3lwcYe2nIf&_f zB;|2OH5{(e_uCF1&^<`>VI2ChsqLorAG>i(*2oc-@G2J&3nWqf^IK7J&&QC@5%?l8 zjhgnS*5c5#@EkegWg1Fm!Ac?G+5D#0zT6=T8sJvCub~g0RIjBAgAPU_wXyEs@ZHC?-9bKHn7q?|DyT%NQwADB9s0KS z1pncm-~Bd{NyzxJfmmcMJB%R~a)cKWMkV3P=Sm_twvZl>`pZw?aS+4F^gD8_{ zTL0)EO1K5DHD;KOBNj?qW0j?|x7^f8;6(%FU_L}<$+d=_94|@pbUEaSO4y=>3Hg@D zO0i^ScJqU!X~poUz~1D<%N8yfrD#&53&O5X8{jub-_f|T!l4~RpSj{_4c#TO;k#i4 z2VmYTmy3emja}VOLu6B{eBCdCq;O(@yI(mk`*x%lL(rK%+sgL=O_8H@Zi0jc6_~|D zU*Q?+4XVlMmKncvH*nnkOIS zqUWKMRPA%K%j6CEAM|JnZ2gJ2DSFq2s~H?2^bh)0is8N=zQJi7nihsEoSEi^4SD4> z0~Hihmd){M##+wFKY*-nwD$2o^<5*r>Apr^)dHnYxTOd&`K@ncKZoLC6V0NXKVk-{ zk4Kn>$1HPH3XsiuEx{9R(8Q=z$7)xiRYWxJYGk6o${*Kx2Z} z@b0&=*N5TDoU@|%3HDO$NnslD;8C9E$BWA6OL+-ok(w2uvm8Vr_4kfc>oy zKp~--MR%BnZHb4Aay@Tl-UH$8crp@+fSyv}uD5=-fzYlOmCqx7TH{xGV~@-}QQud) zff_(|+S8W|SR9TcdK5s|{vI}AQZ5Xo7oGsg>d!@GL_jYiSr9I$E`ZEw9!gDYF4bo& zQmt&v4A^jjuw&!y93rU2J4YfAQjYC_0bU)4h|nCCB*LU z`|jwB8$HsfKcgS(67T8Sy=xJ)WssBX`<9&n|BA?NxyO;TP3?5|Z36ebcmm5_AZ7pl zRqVGPZ1+PJ_joC$2oYH0Xomm!Y40Wybs(B>xJkWZG%{9oRaF%cH8N5WZn-KgcXBRI zK$eE>!<$+3>&x@2!kXPU>~{g};G4+(LDT5lO8Vh`LmuaXn~fwKlbr-V_-7=^3B&3f z*t~bwYgXTVvD(l5mFQvi;*?>#I>EjQVL72z(6Hr8L|y`=ny-BB$xRPNIU&1C^$`J0rFx=>MV4oEHHe6fv;D#Z_P zk6L{BL-=s}7w){bMkmvlkbP?rNJDo+)?Oq0r_9GA`3<7mM+5{ny@o-a-=JRFK+5Ps zA1Dpawhao|Z&hEvj#0elg5k*WM)Fm)+D{MRFV)$P$A^IA#}s*@&Cebd$JH|sgej*y zAktDh6N_zZ0G5N}E zDXxcbMAn>nWDQw!3uApSfEF9fc&Mu~xC(=GyKg(Bqac*r(l_eFgj*g zg6X_ks)xD|wsC!21yRK78$W3*`u3N2ZILHZglJ39>1aZyD|W#0ctI?vvgYgZEAs4e z$w9Xg$6p0e!bj3HtJ=|Fi#?8vlHpwqD@|I)0hs}z@zQ!5edL^_1*1Gu*k`V^7j?*} z3}3t9fTRRgQ;-q~DsgvdGuSo!kN!Y3i$LF+-9ozO8&7OE&!lwP`ynggpQV%eUc)t1 z;!*bj+c*cdP8g7rm(i_^SbV*n@PlylE%JfclZKfMH9pz8h?&J(RE6BezQwT*5p|n@ zE-bev2m)P_D~Up;PVv_~#ZVDQ*&ZLH{N{%U5gfji$x*wmT@l=P!G<96Tu=st3zcH6 z@PyxP-bZ|BQ_DBJ;D)Ns>;vZEIkzdy63;vbvs7zW!$9K>x4z|9)0*q;NqwY*1JbHJ z@+m{HV+DuedTG(9AHM4cCJ(Vt1Q)51EtpfgvG6O4%{Ic`BM3{!i3yFIchtQ^!f!c4 zTtociHWSY|lV1RVpkpdny6Ky%JMXQXDO6zd@(!GkoC!nX?{zmnRdtN49eKCb+@z(hga97-?DT-g1_}j zBmUa@i=UoecOB7f&MXe=%F`9lLxVGrKmtF30NlAdVqPU9DwXi8GSrrm!ONM?dz*CV zn~sHM+A>9{w4^sj6E#XSBeL10;2sX`WW3^?@iEnxI6<0?VyiAo3aWk zwMr8WhWO!h2M@Wgk*q3L3P}Y|mK{+{L0L>%imm^o(o6M-Wx=j+&Ago)e96J*j{)S= zPOd1Z0-^<%5DnzmJd2#VX^BC}B#Wx`TR|@>Ye9eoTW;0GYkhqw)Mw>zp==d7z5c7e;S@lW>*IDYYH{4>G;QzYc3I|k8tobk z7QcNl?cRQSQ^alXHBLG&+s)_yOO^_NCH|!_(}hMCv_y(8jGTRi+E(RNr|BwWK3Cv& z(qIRjkFLojReClImevRH1@7$2MrhfOPrty!vApcB1GxDqkcWGFSAU0yMuGEZTbZp= z<}&NE+`-zZs?vR^CT;9h^l?4bj7w#uKFU;r3aiF(1G&Pqy}WYCE1E@Pjpuse$$ou_ zLh9^eL2J|6BI{I&4jzFiO#Z>62Y0fe_+_lSjgM27Q)P!%H_9a6$VpWm-i~ov+nydo zUm0iF@D2Z;5TQ(-OM6sjMf2@?@z{DsM&{tM$}kFivp;|B`zbNi7O)s<*DDHv->W3; z^{oqFHTko{z3LM@@)DBH@&JC|C@_Vqk`LLb3Mu9n{8Ms|H;PnOSnjvxaP1_QM-2Ks z?l_M{y50$*13q6+q!vMEd&Lt#YNR)oxh={TRskf+&)!12F86`D`;(S8R7E*`1lC2C zWnt&0$`|2zqqMz)pThgJPe%@+@P&_%>!zkVmI{iQbD5tEG#2$?whyIGEwkQ)>wr!`)t~PUF(CTcypy+`=d-ZRcR$4vk6Ytg$l8s?`|4yi%X}7gb0F5-e zf@hDsf{s_kJ5+6B%?<|EUcpt2m~ntY14{}C#`gbm8-yX|>soDUcL3WdB?_16!>hl9 z%E-ht!>ZcO9I|B7FW%BzLtQ<#%f)3a9~^=mrNz;ocD&U5mPLuVtZg%h@0?eDT_8z8Pml@@{M6)?wYe|5Cr<#pD*7W!Bq$ zcl0(R#?sYp#g7v`+CI#sQ+uqaTz^4{*@{i+FEk@k6fH+j@1(W@8sG^{ugymQgDWlW za#%aKE_Y?1{?!!f4R{L)e>wqpTKyK?qy{t)WFK@;MUP*emMh93Rv&TBv%5U>9MRVH zk8}B7i0q=tJ2Ca!5gA1KGB_KpPD%lOoeODZydSf_D6kbsxr07vSBuBe4dI0HmL1{Em?ZMRrWE5eF) z%4XQ;suXQGT8g33S3$}<+C5v$wk0egw;N;Ur7f1tYpOS5L5Bg*C8+Uj+WxawT*Si} zeBj;+fLIA|h{xy>JhxnxB;t_zi)Hbwa_`fz5;+FRPCRN}DDv$3k08GcI*n3|G$L7V z*{=a^_>{@EWib9-7_6%RmFTtM8FHF)+Jlc&ba!AbybQ+TARsVa4 zqm~gxE)a*g+hCM|sSS6s58h>6z&%u+98&eRKbGf;-^PB~uEMg*_%^_msM1KXkFHZA zMEtCU5IBa{|8B7I3)Q%jir6ej}VC`dJBF;0Sp4A}fP;()4#tLihZPL4*zGJF?7-Y7X&Pad_)_Em;&hxGAvLAPonk$v@19Bx6~6EL^uj z&XjK}l?T;$`($b95$}@AbRin%le{Pr!Q*sr~G?sMT{BORNm~U%H4m z*Cr`JT|y_cfQufRqhCPmb+nA!C-HXuTi8h9)M8K%iL|TUsm$tbQRs=^vE({&7Qhkh zmwLGwX-WOPn1sC%Bm-AVC6)j$1pfLT-uh=Wa@e>6=Y@f>vig$E?gxvJB(foIlVb~- zw|HOK!mkA_o?__&a}jg_pb0uY3fKTbz$t=xBj%dmu(dqj&r+j8k#Am60;)(3Dc}j1 z@adty*7M0i!BpYl#A))cLfE)jTi4q7Pxk5LLXeMR{*><@yA(SGRxMqGu!lhCK_*av zoJ?Wj`A$IOEWh^QTW@W?U&HADnmtc>eYv1^9++4=OkXW-uGh=hRi0Jlo>v9ytXx{4 zA5r~K5Z4@K*_L!MFkSU=WZ`wlU!k@>M>|s(z58P z)%>WN;{g2B7MLN^ni(bBlMhYa6dM=Ch;(1^6O2$opYkR2GgMcUo29lM3Pl1?_rZd* zZiIbRhZnbM1n-gv&Pt6N7!n>A=~$bKd{Cpcb7>mbg1g z4|IR11AMvdBy5D-{hIerp6@J zLMN{0edTInt5WkIQ*%%XslNA=GVlf|?)tvzxt18WK!-T_9J<4(h-A?GWl0cd1f9Vc zm-oKqNd2~^nhr>uz|9og3jpGm;wcH;bI7vOC5Csz@*1t>c*h`z$YY@Drm0yG~An(^lbP%AGRFO%x%DaQ0nS{u&?Y zbouSGnh}!L&)2Rn>0&Yw7&FB{GD|)9XM~w3AC$O>aG1&pbeq~Oo^GPclXjD>V(*H% zuzsjar#h@#ol&J*f#GTr0|E})g==+@5~tWmTP#%ybvU+(M$lcgH{eR6$DrDMxX>~j zrE$;TvQszwCG#tCVc0XT>(hv=x!`Xur1IuBCRrHFP0QhTyoM}5oDg|sCAAIX`>!a8SB%2 zdBy%gcwz}_Z)amxEiRXoWduKl0sul>!!_f?B9F^QT$vvPY=e>nz}n z#c={jGJ7mTT^y|~b%LPH3s?7<4RO+!iw_1KI0G{Fn(7XKUXXX@tzAB^Y9L;bHyQg= zZ(P|f)qS1ap{{E}nRCv|Ztw)N#t5hvB(xpyv+*mf^GHIbFE*c)(0RMC;*IT1X3LDW z^AUkS@;SBhervHbbInefs5G>6amNH$t^hF>F`&N?XwGnFdrb5<=EDQ*HEdh@S*J`i z>7*9fC6U-&3H+fQM-990+h-am1$GkQL)kmgq#W9HXf>gk-Y!#8gk375a{WResoAIF zdH0n+d>b8{*rpsI{hO!I6~J{sjDaduA$E-i_?vdF{wr3WGbPwNcJQ6+)Q zuTxoJEk-z$LdPmLMl#^eNM8+~%dke$>u@{9xO9Xry z5$Vr+yDX|d>Qpm3n#5q3dwUXPXYznonV2Q0A(KRMMDu!S5*;dW|LI@Jhk6iKL_&O< zRSvfCWT9FoRA2D8X8=WeI!f%2a%dF;GKHrs{J7VYJ*qLtN45<^NA9ylB~{f2yyMEy z`t0qD?mcS~)&x5!qDU?R8i1JS(fvC_NtA-ug#hhcUrQP2BU7;yZ6y7qH&@0ySr-zm_Y6}KD;1mtME_DZQVYAYFO4%O`K z`NE*zLXxZ?Z^4ntzHCOkALvDs;tdM#3n%LnHeG6HQs{}xTJbtDL4i6NZFukpi!yHL zjq}Wl@!A^n_SFRh9y8ILO@*_?=ADpsf4D}6L>UXxE;q*l$|er@$2^)s<3W1KwPX&b z5xJ|Lj=f-L6)xECGgC1%H(z&?jzDS?Fu4by5EwZ>dl6&~cNT(4s@sZf%vKX_U-12s z1HyzmJRZ(F8s|GRq5kv4?Gv7<>MtADOa-jZFLCDyW!nC*Vy)`TPvr@hWMjZs()Rh8 zc{7e)6b}U$QZ`QI5nsl4osP#W{mhW|qYvWv-o*S#uHfXCbah#ar79E~ z{k|YZV$iK`xOwwOArWAmws^{M@E|ihfdl-3F|9=_w04cE9B0|Uk}pw*FB7uE2d9uZ zvzsf&?8ww}O*#ByM&`);n<)nef6BA z^h1Ejr1#-Qulwoyaeuptbyp>kf5L8tdaF6?!T{|m z?yMK*Xou;c6SW^`Y4?Y6h3lf>t$~cL<<{BOSvVjpT*H?iz7oSvzcUqXi?10c}lj03ebfL74(`m ztq$0--VoWF|-8X?8%B^P3>ronHR*nm9Wmm!!idTq2kp z?6kx)A-W%l(|sRZ624@H5Zrs5$o=O0`~>9vN%#FG{`vZy$CA&)56hm<&x@Kjw8;m@ z*Zaqd+-@tN&70$WaC#A$l>QNLMNG`{{Y^xY&a=tpU6W3^jvut4U)b8W+!4)6%a*_a zUxmqP0JBoN8+BxE7V4h*(uRSY*a`ws2Q*-O=Y7=~lw~-=!#zB&@xK6+XThZ-&}`Ur zy2h11;~nbF{KxoMo}gkQS)km(R;8tG3bYas%u2@Mv{BuyYf=YtRDaQ()&L}I_M9c2?8;E&8K$czD~2JVo&vO>~VBHsdN0u z7>&bYS0)oQ(+$DyOw3U!BObL?kC^w-`EyKc|0q)xv&cBRn77bak5wHi1d=VGUtDk{ zaR_8AcQ%BQ&5e3b&QnX34SQHmQ1)|CdX$te;)_gKqN`IV2PsvhU06>1_7Zic*v{BQ z-6e}6_P|wAegK$EP zCuoWt#HBevL}PfG@=hJ7FICLs9`YLs*Rb8iP&B$7Z!OgP zyf5_NUA0`)n;>buyftr*X4;&&8!UEi8Wks*eWFoLLIExDTtPAx3mr7TwwX%Xt^m0f znr)TO`?idK)L^~9i%h*ZtH>Mn`ICCSU{-hta$hGF)D@>bCs!mCH_>zqKmk!_98?4R z6`TVB(#jpoDUfbpbC4%bRZN?6wfVqQVOvTs*N&#mI)i?2?)hk^qq}mZkYw6ZG7SWp z&qLqQ#B}{K=8EM+`6pyJEJ)aaj@pn#bk<4?X0b*9FGDr)iX@70@hG9oV2t@ixXeCT z+P_UdM?U2PR%WV9+hmN)aKvg^TgWdCvNk`z07j1(`T?(Pq4)zE9L`nx=KIaF^v z9Y2@i*{#uqu*HnHt&P%<; zm?s$LsjpLau*x{MZm4VZv#+9L2?oH%P|F)*VU0<-;j`6IL+TI+^-#lPtGCoNCOL9= z=2DPrl!Y-93_hZAIay};$L7-qJ(cP+_17`HvQ<(zQFPT4K_6t;PqEzzUiN+@+KDN~ zw;_bXbzu)6@t>*mi4z_eY4i&yc)9?MS01Pej(sWkw zqn>{wpc4Z}8);V183`Phqn-Exz?VP-w_q;C5awHFXIS~hSfco~)ro!p4L=U63gX~b zJ&p)vy{Ze5QxX+_FvGA*raqN!x3Hc#6LZ&8Eh&<0yxcv z1~}{6yLlESS>e$a+i#RI_X=SFD3pBgfkzpBVvjq=AQNff2PFlT9xVETh_HLn7e9zy zDT~^sRB$MS`;;(j0Yhi~9QHQ=q_CUAg)0cf4!?^5knp*3@m~h$ zjQlsx!RoWB*)SZYv)K*8&6lDxK!ljO%BB%4X1ilBg<_u;XJ0bm66Nepz#Qgr-zQg2 zb>f(`ZkQj+>vjfr-424nOiiXb)R>LuRA-HGkJuafH~?JKuFCb%j^Z0ZVLg5?iZ*8W zg%y^j?=+Yb9dCAp<+@WSv5j-zRZc7^#vNBT=(x%iOqi@Pa`xO*sTe3&UC(#b6Zlaq z2HDrCD}z!fdA}HhXPsgQKJ3XK;x2rhKOWpJ{K4;~&mf4j*!drXTz$AMv7w!@RwlXy zA6WNBc%2-(y%cj)NRSgjv?>OW&tHhemiqYh-%eMOcsa3(Fr%pe<}&r(StoWTCnwJ$ zzO#U5HIeS*v8nbk+IPVrk+GQr2&wZejxkknkU_BOhC)ej5L5@E*enq(b|iTY;g|)UK8@S{Qpm1wy~J+A9cQetU}p2+Jrw`DzmFo z94T7etu0iDVe^-`*AX#&`6V<|5b`_q@%x=0hgF46rB{%ybGsW=_pI1;UFv=}^AKp! z9EuSNWuk(Wkz-SxTp5Xw{FWsuT~JaBo0pUCwPpP3EXyUtzx=BLzabtWyP}bTYqX7v z1bVaAw66{No3l&CGrJHEa`zFyjJ)V;3Pfh$Ltq}fBj!QX=CKJT89E076wM`kY4HZ$ zc}w^%_gS9)_Luzk(=YJzTjoC-KHm+VkEv}=`^4i|(aYZ~$A}!sE`X2!3V*+5)9K}A z>t_(LUDMmmjV~b7k{nP9@stUUs416%x5g(kX1uZcB4YmLe84r9(fvlM7F-zS=Fn8p z^P=1}Q`G9*d~&&{!mp@6nLPz!Y~+qJwtN2dzyF&cc`H<3qxD(FIW%AavgKCHW?75o zEOS{5>yv$eu5x}nbhBJq%*cH?BmVGu4? zqv7n#f+r9=GyWooMbgNW+kZ^ZUff)KSC8M-e0NoR`R|obKT~%-L}FNO!%NS(nuF+vFZvJ6v5+# z9yLE;O(+Vig)2fqSos0HCloq)(r4V!P>eKJY_ZRYn1j;yGyw(KrA*2Fuem@!hq?_J zika!Se0lo&pMT3gKYe+c4{|_#V6AU5>1rTkb0g#@=dSy(N)O;B9%6R}eHQ-J~5?@3R zM}qt&LVPWW0G~g8uUl<*0@BMUGk_DW2lDDa9HFNrJ48iPrL%(Y?!LCT5%3Ig{wq5e?+b$E^7HZ9RV$?% z^vk)rnEdsx@H^g`2_1Fba)(7WQq0ZwW*T$ensw9AoEzCqS$8;+qx9-e(z3PHb2|g1 zpRpmAW=UfPWS67?YKqQ4HoVLo?*iH8-KM_TZ|ZxR;pE%=Q;u6I#fde;G?YVGo>jf2 zK9t08*uMxDGw+&vk!KrjmD_!hk#6hBGJEn?->K^=lBEV9U?x~aP8R?yS=`P;73c4j z5?3_1{e>3{XV~k{LE?EU*wLyY3E5Uj=5(|_dNYEH1Yn$P9-qA*u@*#6}8`Sb?L^ z$+KZW;_MOYYs6xyKY^8@e-DHD_j)_wj;?7&!b2L?#iYy?C;$r49A`}TB( zd(W3j_8Tv8{9>{33pfHPtnd^AgD@`K|l=iBTT5Grh{Sxkch$AaZivhacZnZ9H+iXUWpLaqIBveh zPEfva^IN(QTEm^73{E0SeXz!CDT4E+J@lvSJ!!#a@!0UAk|$(jk~%Tdfd)d};{%;Y zNQYW%&i4BHUHdGF;MMpzZQuf7yD9TW1x)00B#PBzurRjQXh{Y&3?{+D{ zNYmPBc+=ft0^>vn7Pjv_Wi0Exo#MC#6KWdj;Ww*?-{PU%h$;F(ve*M;uejKIJBOW5 zh($3B4k8qj=p1v+4?9+Hd3!{cNp>MYrl9~@0(pD*_q7mtd-#%~@!`}|a=HkJ$qo-@ zxao&7=A4nG)emkUfnB*Q4b#VhyR?DUWu>NxlqA~%t2rlDLOeV{hU$bYYJZ-g9=yOf z_?d&adEw!*;5BI!5ywa6rOYQ#B!son{Z`Ds~Cd1 zTp008y!gF5@hXSVcRxT)SUd~^7jIf`5}eGVFysIsEyqH+rjr5Cef7!T3Q0J4uRf{o zSi}i$MJpU^#LygE$07%@xS(zuejHX6Xx?5xCkBpI@{>LNo)*U{Va1pu%`v;J|BiH( zoss(L`>_nG_oD^YyE|Qz;2J{Y-TDr6%dU%rp|E#QTcIU+s_)WnW}@|tFyXYT0f~;d zJGF`&`cDsCfdN!~YHN|D4-d%ZD;B)|*Db|e_cV*Pin9a&L zrB=EQLKz)1QpQRYCbS?YtgS`4s#k~W7P+gJO7Yrm*CL~O(NNrS$>N8NgyVE`ZX9J{ z@OBK2dy9U4v&s$OwEuv)MMiKH{DTd8b7M}n+09yLAQSGr7Iwsg)=Vv?S6tCRqcDu5 zoYn1vOGN8gt)GKBov^9Fa1xOzbXyCx{UA7cUqW(%DgBSVr|eXr&zP=7d;5Gkuv ze|YsTv}^qfV=UZo;{9k@V6-w~EOWzXxnT^s0Z{PZt4+wkwDvAvO)g_iein|gx?%G3 zaEx1@qnQX_pN6>diKv()CRexBOX0jl9k&6@-Z%3XX z$tG_n=j%7WK9AsC1*&V_bbX%6uPWC@*UF0j-cGUM@OCYu78&+4$RVtdP`kD}ZfMeD zY5$?D1jd`Y?mMF&tu?HmnS4>fh33U(_W%UPT|%vKafSay+YEtx}b`E7{DCX?ZXYxGISq*h@&60jQ}Ke`2*MfgAmeJt9||~KJew(CbqyV z&RJkW%Dett!y%?9QINwk7PHLg$$7$<&k?`dYyew#{4NrV{fOAjB6cb?BF*DQRe-l$ z3Y5MmPkP@p>0JQ#zGz8P*>FRKqef&Ju_fp-CSi*!#1gasT<;ihh$bXN`$PL;A49nI zFB6*>>G16DgxHlMOy~~tH;sYuo^bM0Y#7x7=UBhR3ejDjl%qz=prhr_QM2c$xpTDi zG)mqaEo+W3XO1Cbjy_+GE?bW3o7dJwB#s_7IC$9L*kQv%hYgJ!Hj`_A@*rYkV~7o` z`lQ(zlFj0kesP4bp#j2%#|O0v-}cM*9jRLDRI04D_G)Q)0U?>SZUy1*j457QSwXn8 za%boQadfZnV1JO3JIOF3ITR7Zh6W1mA9N)h3jT67BG^gyk89D1BId%cUPFOM*uUyJ zOo(0KNLV{R2&sJe4QlOl@?*M)HqBrJwFz9@;=H(9oEN?saPeDRe<$ol4U<1Pen}T= zb0uHJCGvN=n7h*@xbxzYPP!D%x;STDT&pQvY>V=e+yP+nqEODk;XtsbCABt<2>@F@ zcmqc$m6@@RwQ%Q{^(`20g@W{)!0MVN0ze4!07PiJJ+;ld^_EhLy~GsG@Fpk$(uQzs z`!MMN*rlZu4*Di&G15vx%Az&MlP&rqBp;TEq;!u<BHQO;2ttkWPQpJf1p1$54C2Dm17J@aAGS*qJZx^!wA9A(eNZ#Mik ztUQjxK;4ZfVbS&Te=5!a^K~%9i?!Bs61T&k?++}68Jw5}7){E_JyO-`(3Y!-k&_derILu0li zdG;R-qx1l;`pvD1*+m##(W`{!duGf98CnYkA>A>%ww5urwQW)_pY19+MStze>ko(# z?{@z9`KPr%4OJ)4N`zV|SXN0Ap3uDZkapxQ_H~HE66-M4N1*fLD^!^%(TgGxH#0L6 zzBcm2FN=g%A6cmZh{`8ekQrtzafK%INwXk69r1o;HcfG`lo*W?L^@51(9-KDp^vv4 z^2+gb?5n})@LeD4t9CBdSCus3&9|HHoU9sY(EQUl*-k^cU_hWm;fLZ(!p8-kjkbz& zIrNl&bM!s^3C=6u>ZN~~hK&yLQ*Um$uENd|p7Ft09fOF1cWYh_cb4il?BNh%@y1pq zZOz-^&X2>YLZ{LzOSMq7Fl=F#>|1XKHw;Sfu7cPvqbw@Fs$3ggE4%2I{IKO#980y? zu~bh_e}O=?8!cFa2v+$e|NWZD*(uEa7=kVC;@V^vR~E>I9EA^B^!DIIZx3Dc_RvLd zDAn@pSV$tw`1~jSwhccZq*!>BmvsaQWk(Bnr{9VnYzOaIXX%wCVHk_9s;VYC7-IvT zi68O?Mhyt^IGCVr)J^IrJ6gzz91UY!4_YAikOguNULg071#%Bs1^1A(Z4X}C_K>x0 z4_(`aGBjhjJVHdN>G5LIOAk(Ak2vxCka_3F$K7wfCCIOUY>mzgdDsdi_r}%b3xanQ z4yzM8C%>v(8(k}_P6%UFjJ;uWpwC1Pm--(uy+0(qKW4Ve7KlGqTT}6fH+mto^5tRo zdp*|94dwzO>(`F15M}*NJr=i=KV(O_TLw<6z)eG3Op*Kp9O*Yn3Blzn!1_?%YiN=} zgIoxYyxi)^B8IVcF({)Lkzw}|c{dve>8lcwjw~Q<2BGL$BZ{T+LqeKRKTbc?dXS`v z^5g76@&`3yf1KS6$q$)r`f;}PLO*i~ze+Y8-;Rg%!A*n<`W)A=>0W#FRcZwh3 zA?RG@8)rX;kAr#mLuVC1)@3M+2DO5arVWSv3$h+u!QxEsX}sq?)UWd4yQL|NNGAru|NM4WJh9RT_q4}j+!z`OM_*2oZzTdpy zFQuRdNZ*MtTXNm-Y`?HJ0?AwzJp1Q`9vzwHdf}O$h%IgzN@7AST2PKAu&9C}#X$~r zWx%5nW~g1TDj zylT!Oz-4j({HS0>d`KijXhJ(znzPOoObCHS%;bnQYn-b=A{U0a^`<(4kjkG` z%3OovQkc#`mb!${)kPy}{z3*zPPR`hYLFnX$W^FG_uRU?ac=E^R6tw=!>R<$6abus zZF4N_vm}0Q(|OnqaNdX+r?9WuG0CbMCaheP z0n$Lc%6k20oh;0xODI$hJ;Oj7HyBvnv;`v728mP<$7=Ptaw9`*s5!UANW`1~nY4wo zxyh2&+M=1q6T4wz$i;T6Gjz`|g+y;j7`9e@wDWecmojh>l;L{O0Lgu7C;D#@+^)eG z^8!kikO4~L575}FUjcZdTyJ|82th=|LY47&4+qs=J2TC;0)sWE;YNo+8UWWWnGreqtL`fT9Lzt6htPUNH zc3uV{a4Jdf77#OG=w}I2=N}6LF)d|1a6p)~p2 zI7v3j;gc4z0D<)(z>qhel8C~C6EI4eXmjOIA97ViVN0S-^C>YEgu$WmB^_Z&(TJoO zivSZBNfPvgR_i@8fo+Aq_lkKW1Q4ek*45%4K(8(PjG*_laL@{*-X|b&RcwS2A?c9N zcS`tlDHoH2T2a zD9-Vf6{+==e%WT6i-IRuc9snyAZk5QpW)W+926$jj)pNykuhHg@Fl)+QY#Wei8RaM z*tV?R;!ibbZbGC!0}$;;BAFzSh)|QIjtxMWUl)DsntI6f*MrGcG!WzL)v0(y^I4K2 z$g~GqWY-vDMhlr*de!yWPx~Oe@I#C-&=YlMfyv@~3SfnEQ8@wU)}ghCm<-TDWs_^l9}JAA zRuRh~28GfnB*J_5N-Yx*`$SZ)_Vr#9L1LGsLp>!Q7)1E-BhPwICD;t#`B#xBm39F} z6fJlm;-t$snfR8*Z)xyWUvsA-;N1M^c0{V$xy#+Mx%>K`f!{fm=&tI|At^8)y|C9J zrc_I9b2!yek-0)Omlx5)w{+6c3Z;f`&EPZ4mWFk5Lw815^~aQVLr$MJkEeBtzR@HI zh53Vpm=keRf({!S5z}B=Uc``w>X?Q3DUzfW#wez-#afqyQ~PbGLbNee1n1(XY>x6R ztA$&4KQ|&RGjw(SD9{CEPV*tO54DAB%r>9onO%lCxRJyRXFEmPbZe~0E=v?^Uq1t|a>0KQv*~kv zpHF_=y1q*vAG%#NJZ%&n#MIT33?$Zl?ly$CZRoPL&Mn?DV=d&2tp4sBbVVl+=A12Aruk@~V}6x%*oVbY`Ivf*6eFBMw78yJ4n@9+nW%IErBogbTj z@6-TxGw~>6l)c9*P2!p_S@QLqpE&mi&tUc7=l(K+K~F7@aEz660Su&a&u7rN;4E%# zc!;4nB}2HGQ(~5|GyXv{S~c9h%w-1jOJh07jJfLSQrl2Aw1#Ua5{D~if7<``6G>7C z*{E2pZI!{AT0^1%aF9Ane*pYRs9@Zii(=PfuyYX+nC~ae1X*nA-qoGbn}jYMcj%}) zH^EwbgUI+@ajipUUIR(S%_eXD@3;Vaj4gNvYYr#`i6?KKv% z%((A{Q5?b^QE@BNZ9gH|d$)S^o2?B-BnwE);dlr^4SC{H^IXH&F0-p$Dvk%FmU4kKgIp@gpaXF4p^9a~{$O z1~W*4;R_bQCKR?%$`~i*j4-ttT6XaSe-1W;f~L-nW>FRjf=tCI2wd%=Xp*LzPEY3m z5pRs;&;uwlFf8kE17d5vBD9!CC^4Ubja=ovmQ=i*)^(jUyq;NtT$V+e-eEz zOQlaQLm)r@%a`Y*P3{!>B+yw-;d{bJY!cooF$t3xN=bMzQJVXw>k%dUAQvh042xwIP9r9x)d=9)%nw_(ep5z8?CW2ov znT1o-o(=yYh8j2>&XK_b(?!R#WFXA=fC;iBjr=Tdtpriry9wOXpIci_K}eG?q7X;S zs51y_9QEg-M+yw*Cs>w(znZjqZG)%YTySkw)U8UyD)5p;95zyx;*DpRxW$Bub?@mo$CQLZ(og2d^>@(KuJ)!;*`v zZW(q)eSusOOZ0oFV{l+%sdkLGxJ*mw&;}nDX=tB< zAhn$Og767Su@AX|{N(jWZS}(k7BF_In=&@8=qQ&(A=!>wm^fwD!{a&)rAnC9Bx0Z5 zHMIxi6`)xLcdY&Lx4%D)?8@^!e(IbC9@JzETT{Kl^`6Fn2R}^*OrOq;gXtPj+bl7O zrCT-nRbWUpA0bd~Z6#%T2C1ut>it-xgxkD@1X<=x$d^?~XflbJHjUTlfDz)roZz!= znQ^Vu&sBI4SUH!w!TS$6VT2@K2y@De?hQ$pz_s$V>Qod6pQLZ-6=;>1VeWV~G3+xn znFSd*L)RS8Vb3f&CoE0H5AJx8tnsqwwo3%AJXsp5;uc-O4l>WYsfb-YM7+cvW#~Rg{x{j&0r;I44A12< zDvA=INSn3r`o*yW6kvd0*2!fQ8ymI(sBr{y!yR!hQQfN;jCWT!5YS+R1{0B- zBX9E6Th&|AyxCbV1T^K{s$N}oi!+6UHI3Zt*nR}&BfVAec^>?3C4j95?;b4qO(+#B zLi(g>3=+L-h*0Msj7e4~4PGppkN%sycArG;(i7A-RFH~emO`F2zy8LYrBFozWB`kR z|E`Tp5OgsQ*s{K$!q^)&`~j@9SPJShE6j{J{sxx_v+NdoAmk;l0wfG%LKWpC;o<5r zgZz=N_^}O*!EPx7LA1c9Hw%z^@s@Fwu8{N z>M+!Le;(9XNp}5%+(~PL(WFPCMv(Kdsy!cehGT0phw>NL*HZFKPFRxf-f60v*DcF6qp<>5{~LfJHNLe2_l^pVx-}QIPufjXRWXds1+tB80MnO z9cGAfIEU|f%`GmP&U~!6@GWElr6hugfc7RzVJzA+f0%+~gjHK`Ynbz0WpU36jWQV# zUG0KKl+D8{o?i6~k9TJYmF#kZI}qL}4X$*R#39&OnX%qFrr1tgMe5d!I;+N*g7L3} zhgI?pqu}VAd6MoX5XNOEAQtaJOSG#WJze3fu4mmtnK5ovSnZx8mfik`^|<-+IKvhu zD*s(X%snKU3pT$Uksw=$DPv1Lr`pwTAc1euj-$+RLwY@7yDC3m3TAZ^daF+7W%15C zR7i4rQh?Bg3Y}dEy(5(}m%iDY03aA>;-(TqSDg?e8@~ZwQ#VUppJ9h&SBiM~gp&#q?5Fgw$pDRYg%-+iA`h%aPyizg3b->Q z*!)L0Ie8WF^$5i*fLqh1Nz&v1W6n2oB_LOtWWrX-qxqzV++;Kj+GSV=NrgpL)SEa_ zG@B)gI0w^xbRqVM(JUjIlh@*QI3Y9vOgIpe`N{WGGt@H^vkoqlr`$djYe`nkQFy_^ zzq1g($&3~&8M~Sj5&CbkzCXw`bh%E{BV27RX`ND6tYA^fd`5(>OIDpQDq@_uhtCW6 z8BN(WepG9w-)DRySUoWlS_~iU8!M){=*i*@rL$u234di=F5Pa%}Vi0cYF?A|~`D z9-c(lBTnfwVkOxI(NMZ<^Q-rmkj^?n@3XDdQ8mkP+t*&dka<^ikiv_5$!@#$8m`WykKe_ag?iRx7 zZZ@WSX(2Hem0gelv|bHi>nAO4LlKxxPPQd01Z_P^eoscvSKnb@&y&OIHDz^^@%+ z0@QP=idW?@VM&#*%JF%@F2+IdmG>Z`fp`Ce!wjyEr<4^IHZi=GD?nDCxLtQ$i;ZUx oT`S1-ihuZ|`vcci2B@cckOfbZ>>t{`P6>r+_l$U zd+qgGYwdmZIrrAEcIAD({6Y5{bc`yST<91-Ff6Xz85Y-3_EdI<<z0EIrgMs zacpN;srAmV1;@^@Z#wprVY`WIAD|zb>1a89*z)D#*o_jh8-I87cX3qZN_8PQH)xP2 zhIP6)c6wMHyB>=4qVDO@N`F-+>QNcx%IkHec6Z{re%L3@y<;cU^2w@`(T?p6o1LA(>^F-N1Xk5|#d`v6E7fYS=X}5h^ z2rkC*lxZw`@#z`*yLXJgyZXDvsN@NVY^KG~Pn6kj3<&mw{<)@0b~*JVDniImH|ETBW6+toAf1 z59ef5rQ84->u!gck6wTD8OKia56AehF1$&@bT+4BjCf;c2lXkc-8Y*~Q}(_ou3r(? zQ=*CMDZ#{b_??o*lfq!6{gW}(pU~gMu_uOs7(e~D^Iq=}Pl>F0vrd|5Zf+ymv2Sx3 zgKM-YIclN)j4C*rX-Bn^Qt-*G=bk-E?l(Q|=1>DI3N0l)vJ7%3!*lRxBE? z9DYqcE1hE|xOv!zuic_KU4z;$ZStoO(Cebh{wb8{pJIg5rx+yOFeScr8Ir?C=&;d3 ze!C3e?J^|KoN~Em3rShj3KdCK%0dd88*zsz zEjE`?Y)d^t956VYJ-k+W;=^&0y`?o# z5%vlXZGI%F0*!^XGOc8ipCe}|XUgHDL3y$%2VNbN zr^MeF_rdRr>#sM@!A}dyQ%%|23d++G<#&SebW;w!At+}P<*$SCO{T0j%8jP1{c-R- z!<1D>9@k7c^waQ+8>XyZ6_iU%+5VNFTx-heMM1gFl;Md%d7LTZU4oK&AKo66n-WjM zbQmrfcBs=~A4(oGETS%X%%Oh`O7@slP?E=Ner8az$241KkJ%SI$zu+$1todRviWrO zn1jKSJm&C)K}jBS_+CNzI{n4!+R-|EMNpE*9KJFrPq9UY*9Rqe%!SR%lgAt;-i9H0 z%;DHH8AHJGaV?y!p=R?tPsKos; zXeaa+WQ=P<=+OTOp6lc9Ff6(qo-|S1>dftUuq=mpk{#FnHtKT3v`oNgIo#?QjbO^S zAqsKYdB#g_$8+87D37}xWtQB>HQsbCk3L@*zK^vWmnZe{=YsxqAsZhY9jvcGL ztMG)4x+P>M)oMmjxrd6S<7>$3_8yY2?FZ4JTWx=rcY~kwZNc z_m#5h;?AlWhJ?`g{-EI3w6qr|goZl=4+yzZ_M|ZwVf-M~*FEnv^9+a-nc&$D`>HeCt?#Am?)3WX(x#)cnzdNGP@*bVK+pk)j z48t6+jx&&ctBS3Ea*|`eCSL1k!Jas?2F$@l?;*$Q^Htm875wEfa)Y?5tir3BW2%bC#qls2Y9{b|mL7C&#eWg2_$m>JCvl+1PFywf>Ehuxm zF7>HJOPYgMe;Pb#-89}KD7RUkjSVP)Jy%_yoU7Mj>>2xZ#}+e4wev81O1U4Rw&BHD zwqCaA%ux;sngy)Wddsn3|1OoTQ>?#~%Tq=L`~4~v`${41>x#Vfm|KDUV^r*U3H!|# zs;13%RWgx@KK4%Hex+icChp;JES?8@)uW+Dn(k^hWHlWb`}YL}>}#up{p4qJ+;_?2 zCAM?Roe||%NmjREWN#vBA(ehA`Z^a^?9gznu?Viji0ycu>^-&YIt`%6obOh|O!U$QnR$F} zP|}{$=4*liuC=c-x1)ey^~IE#hnI#NqEmCU=e)S<2UlBmj!v(Da%LtcDKo1OdG<4K zMY6D+RwQ#~wj5?<`sxV9n)zL{oDm2Vv32*(jv`)unQ97(%g=_QSg! zajR!c6!%WN5&_*Bc|Usd{;KgD-1Q0U>D{fkUpfx6{{6GgZT-asR=rGTj!TO-ToAO~ zj_@#I8LhueiCo(5#K3&B*PQHE!(uC1or=Hetk;}Imda8EZHFOyL(7fwhVfCVajO*N zlVqa&u!>`mD8G-2EsHl0d8RyBpE$bJ!jsU$l(haDK0mlq_FjH%P*RkS%syrPwfIC( zW^eEuM3mE)$8~;hSc%7(Ag#Yz#?9U^hF9fzdU}R%ZMXQaJb*qqpD$wWh}c7)Zo|B1 zy||iII%>AKFlfm?ru~iNAFGE357~mO0@B|Rl;j^%1fQ~&8+d%n7PEhRJSfe+8h}21 zI4J*qwlEL=!68qdFvCBtQL**sY=MT4RB3ll_td#tWYTjOF7KL>E#9p{482U}VVJXp z{p@L?Vp&tt8ZBlgtb*ZhpOEZPB>!0KQ_Yb5BkoUIwjjTDeR{Sq zCGC-oe;IPp6O4T?fuf^k3+{d!%1F1TCvS+kKeEO63h`L)%dz4sKan_sCKA+0}@ZjHzUQ0gvT=_`=NJ{_+> zy2w%O9m^{64x+Sqfpc#eIukLr+KI03uzoL?hm$J%Rm^kRJcr&7OIb61UQj^#ZYpk- z?ia=n2M@U3Q^l4AR}L~kcyZj{Gg`|b=MxR4kn=gZ9b*LmCFOiR>TqA+%3%t)R>|N> zF0;kJwR&g5b#aqwQy#XR&e>U`mxf2}Z_h0@e>`|j=r5ktJtE)ZjV}!zaujy7MTkWX zQ|5WvQ8mY+$CeQyyXcM%87k(P5we^#E3dzZM)^5rTkpi3IvRQ|&ujP(L@Oe%czWJL zUA!T9&gyUVMG!(Kj}(LssMs6Gy0;Xb+w`gQ)g!NuG;(a^;rG7kgDM+`qZr&|lelmE-u~l-KUzZJg#bVgod00q~4^vC)lkeSs zHY9^|l`N#kFBG+pD{^e%$@gY$=P`P&JRpsIx}Ec#*xHtAI4+L0)k;^LZvcBD#*~Ep za3|GhHnle#dRI;%B^&Pqx9qhpJN9-ZRdr!ovgXBlarSTzKn9q}zF z-JZ#gLwlzJJ60ZipTna~JI2euk2=O*Q6xLnJ#FTXIyS~nUzW`H?^!7LY~^A6NOvo( zx?^mwE!5pU%J!tI%?qr1JpL2C~=!X z!5`nL;*pS<(a;n=RZ6n{+Wl4A{E_o@Dv#!3NVCQ8yr9f@ctxK4vF)(L9X-}aIYghL zcR}}-kOLmoV&G9m5#CpL^7FN=4~HDBv&)L>Q-YczeeF>}Ngg__*^~9x?h-u7*2~r^ z&&-zx50+i3*s{rvkps(<%dMY@R>_WQ+=n#JV~(;9B|A>fMAOr&#lJ)=`SrM?EBBt_ zdet<{>!KIaD(`r0UJ{hVGrR$c4!>FJmD#T7b+miIn>9$b)_o2~vh`%gQ;wneJs?wJ zzG=QsU+z8OvHr@CobXs}1O?weqGF4H5HaM{f;?|5d5wFN@mET#bdR!ZKJ*{v;>`=x zx;@h%&lp8=5x80(auLU%RU)S~%W%#G%Y z%-QmldQb6~FdG}93A07hk~PnoipR!U_6dcO5L*1XYT6D{Z3RN~-p3XzoXRuhhQyPi zYX4+MkU1 zV_nsz5<^fter5}@pef1v+BxFLmVIDQSWP0wS(TaKb5t1L$5qOcpxE(mYxKG^Cf`4& zckbgV55CVVU|W;-tIjE3tudmulS?a)3(MC0LEf)C$+BaNy{r_k6hB8>^Ks?z&)LZo ze0jf$H9#|VXH7Y&PaLMOR>86!?QCnKh^?!fGbzq4Rc*61qqUs0a^F4zOTI^2e{oQ- zKCQf)iS=t=SU+M&=bKfUUo&gFFEUT{^x68(gjk=*^Vpen3m$on9Ure+jysh(w#}Ba z^{oNcXC#)6^(zJIlOHeW);l%vSPq$oR>l>Cpm-!8R3(ED6k|r_fwI>7Vs3z9$z+}n zt5|2^10LfsBV-;Bf@iBsPMK%%Uez>&YF{8!DKkRW2ZZn@_XR?<>Z6Fv1H~gf7op2k z+x9tDI^z}J0wLcIFaJ+aK!}5tkjG5qK6>AAX2UR>05Mt%-d&?Oi z^MFuo1w!=IM;q(Lx>5omBzu<$gr@iyFVl5dxvqXTC<&qI4Uv?=rf<`vxncdkL@N-g z`lPvG>pj5(LUk0kiGqe6>ACNWTplfnArP|f?{$wF*04p}14fp45<;78ZXkx>F&=4k z8*@aNQDWz^z0Fr!fl#G@5EMrqT2-DWPZW<9gvemlIU%%c_d3K7Ev<7xXuMK2Er#gJ zo}Lhj=T>_A))tGW5anH<<>8P6LdfYF#=cX@u|2A~K4GOg(s`B49Agg!MJNlEyQ+A_ z)n&!LQ=PrZgwd+&4~oceD1Tp!T$J8> zp1l^YR889zJ{LvrAlXK{eN4mf8MKW3-wmC6e}Q7;J-wLlyx;-F8o{8*VaZ^|VM<=z zVZpX;zGvYPW^vf}TRzBk#Ht+T0kf-A4))Q28HatS^_Vj=m~nWdPj7@oVfNziD9DW* z-TLuXPqhY^L9vGC-lNl%iu+4>Z;9OY-1C6W)*xAS^^xFV9^$ZM z;-j`oYq?&f7e5V478o9tQH z`gc%Gn`5eEtY7bt@pSaDkKpM@HU;Z1RXh?romO6hrF;0*�!oky7O(>rXoo=dr%W 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

+ +

Background

+ +

What are docValues? In the index mapping, there is an option to enable or disable docValues for a specific field mapping. However, what does it actually mean to activate or deactivate docValues, and how does it impact the end user? This document aims to address these questions.

+
+	"default_mapping": {
+		"dynamic": true,
+		"enabled": true,
+		"properties": {
+			"loremIpsum": {
+			"enabled": true,
+			"dynamic": false,
+			"fields": [
+				{
+					"name": "loremIpsum",
+					"type": "text",
+					"store": false,
+					"index": true,
+					"include_term_vectors": false,
+					"include_in_all": false,
+					"docvalues": true
+				}
+			]
+		}
+	}
+
+

Enabling docValues will always result in an increase in the size of your Bleve index, leading to a corresponding increase in disk usage. But what advantages can you expect in return? This document also quantitatively assesses this trade-off with a test case.

+ +

In a more general sense, we recommend enabling docValues on a field mapping if you anticipate queries that involve sorting and/or facet operations on that field. It's important to note, though, that sorting and faceting will work irrespective of whether docValues are enabled or not. This may lead you to wonder if there's any real benefit to enabling docValues since you're allocating extra disk space without an apparent return. The real advantage, however, becomes evident in enhanced query response times and reduced memory consumption during active usage. By accepting a minor increase in the disk space used by your Full-Text Search (FTS) index, you can anticipate better performance in handling search requests that involve sorting and faceting.

+ +

Usage

+ +

The initial use of docValues comes into play when sorting is involved. In the search request JSON, there is a field named "sort." This optional "sort" field can have a slice of JSON objects as its value. Each JSON object must belong to one of the following types: +

    +
  • SortDocID
  • +
  • SortScore (which is the default if none is specified)
  • +
  • SortGeoDistance
  • +
  • SortField
  • +
+

+

DocValues are relevant only when any of the JSON objects in the "sort" field are of type SortGeoDistance or SortField. This means that if you expect queries on a field F, where the queries either do not specify a value for the "sort" field or provide a JSON object of type SortDocID or SortScore, enabling docValues will not improve sorting operations, and as a result, query latency will remain unchanged. It's worth noting that the default sorting object, SortScore, does not require docValues to be enabled for any of the field mappings. Therefore, a search request without a sorting operation will not utilize docValues at all.

+
+ + + + + + + + + + + + + + + + + + + + + + +
No Sort ObjectsSortDocIDSortScoreSortFieldSortGeoDistance
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field":"dolor"
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field":"sit_amet"
+  },
+  "sort":[
+    {
+     "by":"id",
+     "desc":true
+    }
+    ],
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field":"sit_amet"
+  },
+  "sort":[
+    {
+     "by":"score",
+    }
+    ],
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field":"sit_amet"
+  },
+  "sort":[
+    {
+     "by":"field",
+     "field":"dolor",
+     "type":"auto",
+     "mode":"min",
+     "missing":"last"
+    }
+    ],
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field": "dolor"
+  },
+  "sort": [
+    {
+      "by": "geo_distance",
+      "field": "sit_amet",
+      "location": [
+        123.223,
+        34.33
+      ],
+      "unit": "km"
+    }
+  ],
+  "size": 10,
+  "from": 0
+}
+			
+
No DocValues usedNo DocValues usedNo DocValues usedDocValues used for field "dolor". Field Mapping for "dolor" may enable docValues.DocValues used, for field "sit_amet". +Field Mapping for "sit_amet" may enable docValues.
+
+

Now, let's consider faceting. The search request object also includes another field called "facets," where you can specify a collection of facet requests, with each request being associated with a unique name. Each of these facet requests can fall into one of three types: +

    +
  • Date range
  • +
  • Numeric range
  • +
  • Term facet
  • +
+Enabling docValues for the fields associated with such facet requests might provide benefits in this context.

+
+ + + + + + + + + + + + + + + + + +
No Facet RequestDate Range FacetNumeric Range FacetTerm Facet
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field": "dolor"
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field": "sit_amet"
+  },
+  "facet": {
+    "facetA": {
+      "size": 1,
+      "field": "dolor",
+      "date_ranges": [
+        {
+          "name": "lorem",
+          "start": "20/August/2001",
+          "end": "22/August/2002",
+          "datetime_parser": "custDT"
+        }
+      ]
+    }
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field": "sit_amet"
+  },
+  "facet": {
+    "facetA": {
+      "size": 1,
+      "field": "dolor",
+      "numeric_ranges":[
+          { 
+            "name":"lorem",
+            "min":22,
+            "max":34
+          }
+        ]
+    }
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field": "sit_amet"
+  },
+  "facet": {
+    "facetA": {
+      "size": 1,
+      "field": "dolor"
+    }
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
No DocValues usedDocValues used for field "dolor". Field Mapping for "dolor" may enable docValues.
+
+ +

In summary, when a search request is received by the Bleve index, it extracts all the fields from the sort objects and facet objects. To potentially benefit from docValues, you should consider enabling docValues for the fields mentioned in SortField and SortGeoDistance sort objects, as well as the fields associated with all the facet objects. By doing so, you can optimize sorting and faceting operations in your search queries.

+ +
+ + + + + + + + + + + + + +
Combo ACombo B
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field": "sit_amet"
+  },
+  "facet": {
+    "facetA": {
+      "size": 1,
+      "field": "dolor",
+      "date_ranges": [
+        {
+          "name": "lorem",
+          "start": "20/August/2001",
+          "end": "22/August/2002",
+          "datetime_parser": "custDT"
+        }
+      ]
+    }
+  },
+  "sort":[
+    {
+     "by":"field",
+     "field":"lorem",
+     "type":"auto",
+     "mode":"min",
+     "missing":"last"
+    }
+    ],
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "lorem ipsum",
+    "field": "sit_amet"
+  },
+  "facet": {
+    "facetA": {
+      "size": 1,
+      "field": "dolor",
+      "numeric_ranges":[
+          { 
+            "name":"lorem",
+            "min":22,
+            "max":34
+          }
+        ]
+    }
+  },
+  "sort": [
+    {
+      "by": "geo_distance",
+      "field": "ipsum",
+      "location": [
+        123.223,
+        34.33
+      ],
+      "unit": "km"
+    }
+  ],
+  "size": 10,
+  "from": 0
+}
+			
+
DocValues used for field "dolor" and "lorem". Field Mapping for "dolor" and "lorem" may enable docValues.DocValues used for field "dolor" and "ipsum". Field Mapping for "dolor" and "ipsum" may enable docValues.
+
+ +

Empirical Analysis

+ +

To evaluate our hypothesis, I've set up a sample dataset on my personal computer and I've created two Bleve indexes: one with docvalues enabled for three fields (dummyDate, dummyNumber, and dummyTerm), and another where I've disabled docValues for the same three fields. These field mappings were incorporated into the Default Mapping. It's important to mention that for both indexes, DocValues for dynamic fields were enabled, as the default mapping is dynamic.

+ +

The values for dummyDate and dummyNumber were configured to increase monotonically, with dummyDate representing a date value and `dummyNumber` representing a numeric value. This setup was intentional to ensure that facet aggregation would consistently result in cache hits and misses, providing a useful testing scenario.

+ +
+ + + + + + + + + + + + + +
Index AIndex B
+
+   "default_mapping": {
+    "dynamic": true,
+    "enabled": true,
+    "properties": {
+     "dummyNumber": {
+      "enabled": true,
+      "dynamic": false,
+      "fields": [
+       {
+        "name": "dummyNumber",
+        "type": "text",
+        "store": false,
+        "index": true,
+        "include_term_vectors": false,
+        "include_in_all": false,
+        "docvalues": true
+       }
+      ]
+     },
+     "dummyTerm": {
+      "enabled": true,
+      "dynamic": false,
+      "fields": [
+       {
+        "name": "dummyTerm",
+        "type": "text",
+        "store": false,
+        "index": true,
+        "include_term_vectors": false,
+        "include_in_all": false,
+        "docvalues": true
+       }
+      ]
+     },
+     "dummyDate": {
+      "enabled": true,
+      "dynamic": false,
+      "fields": [
+       {
+        "name": "dummyDate",
+        "type": "text",
+        "store": false,
+        "index": true,
+        "include_term_vectors": false,
+        "include_in_all": false,
+        "docvalues": true
+       }
+      ]
+     }
+    }
+   }
+			
+
+
+   "default_mapping": {
+    "dynamic": true,
+    "enabled": true,
+    "properties": {
+     "dummyNumber": {
+      "enabled": true,
+      "dynamic": false,
+      "fields": [
+       {
+        "name": "dummyNumber",
+        "type": "text",
+        "store": false,
+        "index": true,
+        "include_term_vectors": false,
+        "include_in_all": false,
+        "docvalues": false
+       }
+      ]
+     },
+     "dummyTerm": {
+      "enabled": true,
+      "dynamic": false,
+      "fields": [
+       {
+        "name": "dummyTerm",
+        "type": "text",
+        "store": false,
+        "index": true,
+        "include_term_vectors": false,
+        "include_in_all": false,
+        "docvalues": false
+       }
+      ]
+     },
+     "dummyDate": {
+      "enabled": true,
+      "dynamic": false,
+      "fields": [
+       {
+        "name": "dummyDate",
+        "type": "text",
+        "store": false,
+        "index": true,
+        "include_term_vectors": false,
+        "include_in_all": false,
+        "docvalues": false
+       }
+      ]
+     }
+    }
+   }
+			
+
Docvalues enabled across all three field mappingsDocvalues disabled across all three field mappings
+
+ +Document Format used for the test scenario: + +
+ + + + + + + + + + + + +
Document 1Document 2... Document iDocument 5000
+
+{
+	"dummyTerm":"Term",
+	"dummyDate":"2000-01-01T00:00:00,
+	"dummyNumber:1
+}
+			
+
+
+{
+	"dummyTerm":"Term",
+	"dummyDate":"2000-01-01T01:00:00,
+	"dummyNumber:2
+}
+			
+
+
+{
+	"dummyTerm":"Term",
+	"dummyDate":"2000-01-01T01:00:00"+(i hours),
+	"dummyNumber:i
+}
+			
+
+
+{
+	"dummyTerm":"Term",
+	"dummyDate":2000-01-01T01:00:00 + (5000 hours),
+	"dummyNumber:5000
+}
+			
+
+
+ +

Now I ran the following set of search requests across both the indexes, while increasing the number of documents indexed from 2000 to 4000.

+ +
+ + + + + + + + + + + + +
Request 1Request 2... Request iRequest 1000
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "term",
+    "field":"dummyTerm"
+  },
+  "facets":{
+    "myDate":{
+      "field":"dummyDate",
+      "size":100000,
+      "date_ranges":[
+        {
+          "start":"2000-01-01T00:00:00",
+          "end":"2000-01-01T01:00:00"
+        }
+      ]
+    },
+    "myNum":{
+      "field":"dummyNumber",
+      "size":100000,
+      "numeric_ranges":[
+        {
+          "min": 1000,
+          "max": 1001
+        }
+      ]
+    }
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "term",
+    "field":"dummyTerm"
+  },
+  "facets":{
+    "myDate":{
+      "field":"dummyDate",
+      "size":100000,
+      "date_ranges":[
+        {
+          "start":"2000-01-01T01:00:00",
+          "end":"2000-01-01T02:00:00"
+        }
+      ]
+    },
+    "myNum":{
+      "field":"dummyNumber",
+      "size":100000,
+      "numeric_ranges":[
+        {
+          "min": 999,
+          "max": 1000
+        }
+      ]
+    }
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "term",
+    "field":"dummyTerm"
+  },
+  "facets":{
+    "myDate":{
+      "field":"dummyDate",
+      "size":100000,
+      "date_ranges":[
+        {
+          "start":"2000-01-01T00:00:00" + i hour
+          "end":"2000-01-01T00:00:00" + (i+1) hour
+        }
+      ]
+    },
+    "myNum":{
+      "field":"dummyNumber",
+      "size":100000,
+      "numeric_ranges":[
+        {
+          "min": 1000-i,
+          "max": 1000-i+1
+        }
+      ]
+    }
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+{
+  "explain": true,
+  "fields": [
+    "*"
+  ],
+  "highlight": {},
+  "query": {
+    "match": "term",
+    "field":"dummyTerm"
+  },
+  "facets":{
+    "myDate":{
+      "field":"dummyDate",
+      "size":100000,
+      "date_ranges":[
+        {
+          "start":"2000-01-01T01:00:00" + 1000 hour,
+          "end":"2000-01-01T02:00:00" + 1001 hour
+        }
+      ]
+    },
+    "myNum":{
+      "field":"dummyNumber",
+      "size":100000,
+      "numeric_ranges":[
+        {
+          "min": 0,
+          "max": 1
+        }
+      ]
+    }
+  },
+  "size": 10,
+  "from": 0
+}
+			
+
+
+ + +
+ + + + + + + + +
Bleve index size growth with increase in indexed documentsTotal query time for 1000 queries with increase in number of indexed documents
indexSizeVsNumDocs.pngqueryTimevsNumDocs.png
+
+ +
+ + + + + + + + + +
Average increase in index size (in bytes) by enabling DocValuesAverage reduction in time taken to perform 1000 queries (in milliseconds) by enabling DocValues
7762.4727.034
+Even at this small scale, with a small document size and a very limited number of indexed documents, we still observe a noticeable tradeoff. With just a slight increase in the index size (an average of 7KB) we obtain a 20ms reduction in the total execution time, on average, for only 1000 queries. + +

Technical Information

+ +

When a search request involves facet or sorting operations on a field F, these operations occur after the main search query is executed. For instance, if the main query yields a result of 200 documents, the sorting and faceting processes will be applied to these 200 documents. However, the main query result only provides a set of document IDs, not the actual document contents.

+ +

Here's where docValues become essential. If the field mapping for F is docValue enabled, the system can directly access the values for the field from the stored docValue part in the index file. This means that for each document ID returned in the search result, the field values are readily available.

+ +

However, if docValues are not enabled for field F, the system must take a different approach. It needs to "fetch the document" from the index file, read the value for field F, and cache this field-document pair in memory for further processing. The issue becomes apparent in the latter scenario. By not enabling docValues for field F, you essentially retrieve all the documents in the search result (at the worst case), which can be a substantial amount of data. Moreover, you have to cache this information in memory, leading to increased memory usage. As a result, query latency significantly suffers because you're essentially fetching and processing all documents, which can be both time-consuming and resource-intensive. Enabling docValues for the relevant fields is, therefore, a crucial optimization to enhance query performance and reduce memory overhead in such situations.

diff --git a/docs/sort_facet_supporting_docs/indexSizeVsNumDocs.png b/docs/sort_facet_supporting_docs/indexSizeVsNumDocs.png new file mode 100644 index 0000000000000000000000000000000000000000..11211709d5faa1e6d27e8a493f49ef532cc56de3 GIT binary patch literal 30918 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
V|TsP3pYZt!h;8}F%vpSjR7x+zTi9Wq}gk^?K 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 literal 0 HcmV?d00001 diff --git a/docs/sort_facet_supporting_docs/queryTimevsNumDocs.png b/docs/sort_facet_supporting_docs/queryTimevsNumDocs.png new file mode 100644 index 0000000000000000000000000000000000000000..151b422aa7faad3213a54b55007225a564b6fccd GIT binary patch literal 32162 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 a;wYfGS1L literal 0 HcmV?d00001 diff --git a/docs/synonyms.md b/docs/synonyms.md new file mode 100644 index 0000000..0777deb --- /dev/null +++ b/docs/synonyms.md @@ -0,0 +1,180 @@ +# Synonym search + +* *v2.5.0* (and after) will come with support for **synonym definition indexing and search**. +* We've achieved this by embedding synonym indexes within our bleve (scorch) indexes. +* Usage of zap file format: [v16](https://github.com/blevesearch/zapx/blob/master/zap.md). Here we co-locate text, vector and synonym indexes as neighbors within segments, continuing to conform to the segmented architecture of *scorch*. + +## Supported + +* Indexing `Synonym Definitions` allows specifying equivalent terms that will be used to construct the synonym index. There are currently two types of `Synonym Definitions` supported: + + 1. Equivalent Mapping: + + In this type, all terms in the *synonyms* list are considered equal and can replace one another. Any of these terms can match a query or document containing any other term in the group, ensuring full synonym coverage. + + ```json + { + "synonyms": [ + "tranquil", + "peaceful", + "calm", + "relaxed", + "unruffled" + ] + } + ``` + + 2. Explicit Mapping: + + In this mapping, only the terms in the *input* list ("blazing") will have the terms in *synonyms* as their synonyms. The input terms are not equivalent to each other, and the synonym relationship is explicitly directional, applying only from the *input* to the *synonyms*. + + ```json + { + "input": [ + "blazing" + ], + "synonyms": [ + "intense", + "radiant", + "burning", + "fiery", + "glowing" + ] + } + ``` + +* The addition of `Synonym Sources` in the index mapping enables associating a set of `synonym definitions` (called a `synonym collection`) with a specific analyzer. This allows for preprocessing of terms in both the *input* and *synonyms* lists before the synonym index is created. By using an analyzer, you can normalize or transform terms (e.g., case folding, stemming) to improve synonym matching. + + ```json + { + "analysis": { + "synonym_sources": { + "english": { + "collection": "en_thesaurus", + "analyzer": "en" + }, + "german": { + "collection": "de_thesaurus", + "analyzer": "de" + } + } + } + } + ``` + + There are two `synonym sources` named "english" and "german," each associated with its respective `synonym collection` and analyzer. In any text field mapping, a `synonym source` can be specified to enable synonym expansion when the field is queried. The analyzer of the synonym source must match the analyzer of the field mapping to which it is applied. + +* Any text-based Bleve query (e.g., match, phrase, term, fuzzy, etc.) will use the `synonym source` (if available) for the queried field to expand the search terms using the thesaurus created from user-defined synonym definitions. The behavior for specific query types is as follows: + + 1. Queries with `fuzziness` parameter: For queries like match, phrase, and match-phrase that support the `fuzziness` parameter, the queried terms are fuzzily matched with the thesaurus's LHS terms to generate candidate terms. These terms are then combined with the results of fuzzy matching against the field dictionary, which contains the terms present in the queried field. + + 2. Wildcard, Regexp, and Prefix queries: These queries follow a similar approach. First, the thesaurus is used to expand terms (e.g., LHS terms that match the prefix or regex). The resulting terms are then combined with candidate terms from dictionary expansion. + +## Indexing + +Below is an example of using the Bleve API to define synonym sources, index synonym definitions, and associate them with a text field mapping: + +```go +// Define a document to be indexed. +doc := struct { + Text string `json:"text"` +}{ + Text: "hardworking employee", +} + +// Define a synonym definition where "hardworking" has equivalent terms. +synDef := &bleve.SynonymDefinition{ + Synonyms: []string{ + "hardworking", + "industrious", + "conscientious", + "persistent", + }, +} + +// Define the name of the `synonym collection`. +// This collection groups multiple synonym definitions. +synonymCollection := "collection1" + +// Define the name of the `synonym source`. +// This source will be associated with specific field mappings. +synonymSourceName := "english" + +// Define the analyzer to process terms in the synonym definitions. +// This analyzer must match the one applied to the field using the synonym source. +analyzer := "en" + +// Configure the synonym source by associating it with the synonym collection and analyzer. +synonymSourceConfig := map[string]interface{}{ + "collection": synonymCollection, + "analyzer": analyzer, +} + +// Create a new index mapping. +bleveMapping := bleve.NewIndexMapping() + +// Add the synonym source configuration to the index mapping. +err := bleveMapping.AddSynonymSource(synonymSourceName, synonymSourceConfig) +if err != nil { + panic(err) +} + +// Create a text field mapping with the specified analyzer and synonym source. +textFieldMapping := bleve.NewTextFieldMapping() +textFieldMapping.Analyzer = analyzer +textFieldMapping.SynonymSource = synonymSourceName + +// Associate the text field mapping with the "text" field in the default document mapping. +bleveMapping.DefaultMapping.AddFieldMappingsAt("text", textFieldMapping) + +// Create a new index with the specified mapping. +index, err := bleve.New("example.bleve", bleveMapping) +if err != nil { + panic(err) +} + +// Index the document into the created index. +err = index.Index("doc1", doc) +if err != nil { + panic(err) +} + +// Check if the index supports synonym indexing and add the synonym definition. +if synIndex, ok := index.(bleve.SynonymIndex); ok { + err = synIndex.IndexSynonym("synDoc1", synonymCollection, synDef) + if err != nil { + panic(err) + } +} else { + // If the index does not support synonym indexing, raise an error. + panic("expected synonym index") +} +``` + +## Querying + +```go +// Query the index created above. +// Create a match query for the term "persistent". +query := bleve.NewMatchQuery("persistent") + +// Specify the field to search within, in this case, the "text" field. +query.SetField("text") + +// Create a search request with the query and enable explanation to understand how results are scored. +searchRequest := bleve.NewSearchRequest(query) +searchRequest.Explain = true + +// Execute the search on the index. +searchResult, err := index.Search(searchRequest) +if err != nil { + // Handle any errors that occur during the search. + panic(err) +} + +// The search result will contain one match: "doc1". This document includes the term "hardworking", +// which is a synonym for the queried term "persistent". The synonym relationship is based on +// the user-defined thesaurus associated with the index. +// Print the search results, which will include the explanation for the match. +fmt.Println(searchResult) +``` diff --git a/docs/vectors.md b/docs/vectors.md new file mode 100644 index 0000000..1c2917f --- /dev/null +++ b/docs/vectors.md @@ -0,0 +1,149 @@ +# Nearest neighbor (vector) search + +* *v2.4.0* (and after) will come with support for **vectors' indexing and search**. +* We've achieved this by embedding [FAISS](https://github.com/facebookresearch/faiss) indexes within our bleve (scorch) indexes. +* Introduction of a new zap file format: [v16](https://github.com/blevesearch/zapx/blob/master/zap.md) - which will be the default going forward. Here we co-locate text and vector indexes as neighbors within segments, continuing to conform to the segmented architecture of *scorch*. + +## Pre-requisite(s) + +* Induction of [FAISS](https://github.com/blevesearch/faiss) into our eco system, which is a fork of the original [facebookresearch/faiss](https://github.com/facebookresearch/faiss) +* FAISS is a C++ library that needs to be compiled and it's shared libraries need to be situated at an accessible path for your application. +* A `vectors` GO TAG needs to be set for bleve to access all the supporting code. This TAG must be set only after the FAISS shared library is made available. Failure to do either will inhibit you from using this feature. +* Please follow these [instructions](#setup-instructions) below for any assistance in the area. +* Releases of `blevesearch/bleve` work with select checkpoints of `blevesearch/faiss` owing to API changes and improvements (tracking over the `bleve` branch): + | bleve version(s) | blevesearch/faiss version | + | --- | --- | + | `v2.4.0` | [blevesearch/faiss@7b119f4](https://github.com/blevesearch/faiss/tree/7b119f4b9c408989b696b36f8cc53908e53de6db) (modified v1.7.4) | + | `v2.4.1`, `v2.4.2` | [blevesearch/faiss@d9db66a](https://github.com/blevesearch/faiss/tree/d9db66a38518d99eb334218697e1df0732f3fdf8) (modified v1.7.4) | + | `v2.4.3`, `v2.4.4` | [blevesearch/faiss@b747c55](https://github.com/blevesearch/faiss/tree/b747c55a93a9627039c34d44b081f375dca94e57) (modified v1.8.0) | + | `v2.5.0`, `v2.5.1` | [blevesearch/faiss@352484e](https://github.com/blevesearch/faiss/tree/352484e0fc9d1f8f46737841efe5f26e0f383f71) (modified v1.10.0) | + +## Supported + +* The `vector` field type is an array that is to hold float32 values only. +* The `vector_base64` field type to support base64 encoded strings using little endian byte ordering (v2.4.1+) +* Supported similarity metrics are: [`"cosine"` (v2.4.3+), `"dot_product"`, `"l2_norm"`]. + * `cosine` paths will additionally normalize vectors before indexing and search. +* Supported dimensionality is between 1 and 2048 (v2.4.0), and up to **4096** (v2.4.1+). +* Supported vector index optimizations: `latency`, `memory_efficient` (v2.4.1+), `recall`. +* Vectors from documents that do not conform to the index mapping dimensionality are simply discarded at index time. +* The dimensionality of the query vector must match the dimensionality of the indexed vectors to obtain any results. +* Pure kNN searches can be performed, but the `query` attribute within the search request must be set - to `{"match_none": {}}` in this case. The `query` attribute is made optional when `knn` is available with v2.4.1+. +* Hybrid searches are supported, where results from `query` are unioned (for now) with results from `knn`. The tf-idf scores from exact searches are simply summed with the similarity distances to determine the aggregate scores. +``` +aggregate_score = (query_boost * query_hit_score) + (knn_boost * knn_hit_distance) +``` +* Multi kNN searches are supported - the `knn` object within the search request accepts an array of requests. These sub objects are unioned by default but this behavior can be overriden by setting `knn_operator` to `"and"`. +* Previously supported pagination settings will work as they were, with size/limit being applied over the top-K hits combined with any exact search hits. + +## Indexing + +```go +doc := struct{ + Id string `json:"id"` + Text string `json:"text"` + Vec []float32 `json:"vec"` +}{ + Id: "example", + Text: "hello from united states", + Vec: []float32{0,1,2,3,4,5,6,7,8,9}, +} + +textFieldMapping := mapping.NewTextFieldMapping() +vectorFieldMapping := mapping.NewVectorFieldMapping() +vectorFieldMapping.Dims = 10 +vectorFieldMapping.Similarity = "l2_norm" // euclidean distance + +bleveMapping := bleve.NewIndexMapping() +bleveMapping.DefaultMapping.Dynamic = false +bleveMapping.DefaultMapping.AddFieldMappingsAt("text", textFieldMapping) +bleveMapping.DefaultMapping.AddFieldMappingsAt("vec", vectorFieldMapping) + +index, err := bleve.New("example.bleve", bleveMapping) +if err != nil { + panic(err) +} +index.Index(doc.Id, doc) +``` + +## Querying + +```go +searchRequest := NewSearchRequest(query.NewMatchNoneQuery()) +searchRequest.AddKNN( + "vec", // vector field name + []float32{10,11,12,13,14,15,16,17,18,19}, // query vector (same dims) + 5, // k + 0, // boost +) +searchResult, err := index.Search(searchRequest) +if err != nil { + panic(err) +} +fmt.Println(searchResult.Hits) +``` + +## Querying with filters (v2.4.3+) + +```go +searchRequest := NewSearchRequest(query.NewMatchNoneQuery()) +filterQuery := NewTermQuery("hello") +searchRequest.AddKNNWithFilter( + "vec", // vector field name + []float32{10,11,12,13,14,15,16,17,18,19}, // query vector (same dims) + 5, // k + 0, // boost + filterQuery, // filter query +) +searchResult, err := index.Search(searchRequest) +if err != nil { + panic(err) +} +fmt.Println(searchResult.Hits) +``` + +## Setup Instructions + +* Using `cmake` is a recommended approach by FAISS authors. +* More details here - [faiss/INSTALL](https://github.com/blevesearch/faiss/blob/main/INSTALL.md). + +### Linux + +Also documented here - [go-faiss/README](https://github.com/blevesearch/go-faiss/blob/master/README.md). + +``` +git clone https://github.com/blevesearch/faiss.git +cd faiss +cmake -B build -DFAISS_ENABLE_GPU=OFF -DFAISS_ENABLE_C_API=ON -DBUILD_SHARED_LIBS=ON . +make -C build +sudo make -C build install +``` + +Building will produce the dynamic library `faiss_c`. You will need to install it in a place where your system will find it (e.g. /usr/lib). You can do this with: +``` +sudo cp build/c_api/libfaiss_c.so /usr/local/lib +``` + +### OSX + +While you shouldn't need to do any different over osX x86_64, with aarch64 - some instructions need adjusting (see [facebookresearch/faiss#2111](https://github.com/facebookresearch/faiss/issues/2111)) .. + +``` +LDFLAGS="-L/opt/homebrew/opt/llvm/lib" CPPFLAGS="-I/opt/homebrew/opt/llvm/include" CXX=/opt/homebrew/opt/llvm/bin/clang++ CC=/opt/homebrew/opt/llvm/bin/clang cmake -B build -DFAISS_ENABLE_GPU=OFF -DFAISS_ENABLE_C_API=ON -DBUILD_SHARED_LIBS=ON -DFAISS_ENABLE_PYTHON=OFF . +make -C build +sudo make -C build install +sudo cp build/c_api/libfaiss_c.dylib /usr/local/lib +``` + +### Sanity check + +Once the supporting library is built and made available, a sanity run is recommended to make sure all unit tests and especially those accessing the vectors' code pass. Here's how .. + +``` +export DYLD_LIBRARY_PATH=/usr/local/lib +go test -v ./... --tags=vectors +``` +-or- +``` +go test -ldflags "-r /usr/local/lib" ./... -tags=vectors +``` diff --git a/document/document.go b/document/document.go new file mode 100644 index 0000000..569d57b --- /dev/null +++ b/document/document.go @@ -0,0 +1,159 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeDocument int + +func init() { + var d Document + reflectStaticSizeDocument = int(reflect.TypeOf(d).Size()) +} + +type Document struct { + id string `json:"id"` + Fields []Field `json:"fields"` + CompositeFields []*CompositeField + StoredFieldsSize uint64 + indexed bool +} + +func (d *Document) StoredFieldsBytes() uint64 { + return d.StoredFieldsSize +} + +func NewDocument(id string) *Document { + return &Document{ + id: id, + Fields: make([]Field, 0), + CompositeFields: make([]*CompositeField, 0), + } +} + +func NewSynonymDocument(id string) *Document { + return &Document{ + id: id, + Fields: make([]Field, 0), + } +} + +func (d *Document) Size() int { + sizeInBytes := reflectStaticSizeDocument + size.SizeOfPtr + + len(d.id) + + for _, entry := range d.Fields { + sizeInBytes += entry.Size() + } + + for _, entry := range d.CompositeFields { + sizeInBytes += entry.Size() + } + + return sizeInBytes +} + +func (d *Document) AddField(f Field) *Document { + switch f := f.(type) { + case *CompositeField: + d.CompositeFields = append(d.CompositeFields, f) + default: + d.Fields = append(d.Fields, f) + } + return d +} + +func (d *Document) GoString() string { + fields := "" + for i, field := range d.Fields { + if i != 0 { + fields += ", " + } + fields += fmt.Sprintf("%#v", field) + } + compositeFields := "" + for i, field := range d.CompositeFields { + if i != 0 { + compositeFields += ", " + } + compositeFields += fmt.Sprintf("%#v", field) + } + return fmt.Sprintf("&document.Document{ID:%s, Fields: %s, CompositeFields: %s}", d.ID(), fields, compositeFields) +} + +func (d *Document) NumPlainTextBytes() uint64 { + rv := uint64(0) + for _, field := range d.Fields { + rv += field.NumPlainTextBytes() + } + for _, compositeField := range d.CompositeFields { + for _, field := range d.Fields { + if compositeField.includesField(field.Name()) { + rv += field.NumPlainTextBytes() + } + } + } + return rv +} + +func (d *Document) ID() string { + return d.id +} + +func (d *Document) SetID(id string) { + d.id = id +} + +func (d *Document) AddIDField() { + d.AddField(NewTextFieldCustom("_id", nil, []byte(d.ID()), index.IndexField|index.StoreField, nil)) +} + +func (d *Document) VisitFields(visitor index.FieldVisitor) { + for _, f := range d.Fields { + visitor(f) + } +} + +func (d *Document) VisitComposite(visitor index.CompositeFieldVisitor) { + for _, f := range d.CompositeFields { + visitor(f) + } +} + +func (d *Document) HasComposite() bool { + return len(d.CompositeFields) > 0 +} + +func (d *Document) VisitSynonymFields(visitor index.SynonymFieldVisitor) { + for _, f := range d.Fields { + if sf, ok := f.(index.SynonymField); ok { + visitor(sf) + } + } +} + +func (d *Document) SetIndexed() { + d.indexed = true +} + +func (d *Document) Indexed() bool { + return d.indexed +} diff --git a/document/document_test.go b/document/document_test.go new file mode 100644 index 0000000..22aff7a --- /dev/null +++ b/document/document_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "testing" +) + +func TestDocumentNumPlainTextBytes(t *testing.T) { + + tests := []struct { + doc *Document + num uint64 + }{ + { + doc: NewDocument("a"), + num: 0, + }, + { + doc: NewDocument("b"). + AddField(NewTextField("name", nil, []byte("hello"))), + num: 5, + }, + { + doc: NewDocument("c"). + AddField(NewTextField("name", nil, []byte("hello"))). + AddField(NewTextField("desc", nil, []byte("x"))), + num: 6, + }, + { + doc: NewDocument("d"). + AddField(NewTextField("name", nil, []byte("hello"))). + AddField(NewTextField("desc", nil, []byte("x"))). + AddField(NewNumericField("age", nil, 1.0)), + num: 14, + }, + { + doc: NewDocument("e"). + AddField(NewTextField("name", nil, []byte("hello"))). + AddField(NewTextField("desc", nil, []byte("x"))). + AddField(NewNumericField("age", nil, 1.0)). + AddField(NewCompositeField("_all", true, nil, nil)), + num: 28, + }, + { + doc: NewDocument("e"). + AddField(NewTextField("name", nil, []byte("hello"))). + AddField(NewTextField("desc", nil, []byte("x"))). + AddField(NewNumericField("age", nil, 1.0)). + AddField(NewCompositeField("_all", true, nil, []string{"age"})), + num: 20, + }, + } + + for _, test := range tests { + actual := test.doc.NumPlainTextBytes() + if actual != test.num { + t.Errorf("expected doc '%s' to have %d plain text bytes, got %d", test.doc.ID(), test.num, actual) + } + } +} diff --git a/document/field.go b/document/field.go new file mode 100644 index 0000000..eb104e2 --- /dev/null +++ b/document/field.go @@ -0,0 +1,45 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + index "github.com/blevesearch/bleve_index_api" +) + +type Field interface { + // Name returns the path of the field from the root DocumentMapping. + // A root field path is "field", a subdocument field is "parent.field". + Name() string + // ArrayPositions returns the intermediate document and field indices + // required to resolve the field value in the document. For example, if the + // field path is "doc1.doc2.field" where doc1 and doc2 are slices or + // arrays, ArrayPositions returns 2 indices used to resolve "doc2" value in + // "doc1", then "field" in "doc2". + ArrayPositions() []uint64 + Options() index.FieldIndexingOptions + Analyze() + Value() []byte + + // NumPlainTextBytes should return the number of plain text bytes + // that this field represents - this is a common metric for tracking + // the rate of indexing + NumPlainTextBytes() uint64 + + Size() int + + EncodedFieldType() byte + AnalyzedLength() int + AnalyzedTokenFrequencies() index.TokenFrequencies +} diff --git a/document/field_boolean.go b/document/field_boolean.go new file mode 100644 index 0000000..fdd14ce --- /dev/null +++ b/document/field_boolean.go @@ -0,0 +1,142 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeBooleanField int + +func init() { + var f BooleanField + reflectStaticSizeBooleanField = int(reflect.TypeOf(f).Size()) +} + +const DefaultBooleanIndexingOptions = index.StoreField | index.IndexField | index.DocValues + +type BooleanField struct { + name string + arrayPositions []uint64 + options index.FieldIndexingOptions + value []byte + numPlainTextBytes uint64 + length int + frequencies index.TokenFrequencies +} + +func (b *BooleanField) Size() int { + var freqSize int + if b.frequencies != nil { + freqSize = b.frequencies.Size() + } + return reflectStaticSizeBooleanField + size.SizeOfPtr + + len(b.name) + + len(b.arrayPositions)*size.SizeOfUint64 + + len(b.value) + + freqSize +} + +func (b *BooleanField) Name() string { + return b.name +} + +func (b *BooleanField) ArrayPositions() []uint64 { + return b.arrayPositions +} + +func (b *BooleanField) Options() index.FieldIndexingOptions { + return b.options +} + +func (b *BooleanField) Analyze() { + tokens := make(analysis.TokenStream, 0) + tokens = append(tokens, &analysis.Token{ + Start: 0, + End: len(b.value), + Term: b.value, + Position: 1, + Type: analysis.Boolean, + }) + + b.length = len(tokens) + b.frequencies = analysis.TokenFrequency(tokens, b.arrayPositions, b.options) +} + +func (b *BooleanField) Value() []byte { + return b.value +} + +func (b *BooleanField) Boolean() (bool, error) { + if len(b.value) == 1 { + return b.value[0] == 'T', nil + } + return false, fmt.Errorf("boolean field has %d bytes", len(b.value)) +} + +func (b *BooleanField) GoString() string { + return fmt.Sprintf("&document.BooleanField{Name:%s, Options: %s, Value: %s}", b.name, b.options, b.value) +} + +func (b *BooleanField) NumPlainTextBytes() uint64 { + return b.numPlainTextBytes +} + +func (b *BooleanField) EncodedFieldType() byte { + return 'b' +} + +func (b *BooleanField) AnalyzedLength() int { + return b.length +} + +func (b *BooleanField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return b.frequencies +} + +func NewBooleanFieldFromBytes(name string, arrayPositions []uint64, value []byte) *BooleanField { + return &BooleanField{ + name: name, + arrayPositions: arrayPositions, + value: value, + options: DefaultBooleanIndexingOptions, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewBooleanField(name string, arrayPositions []uint64, b bool) *BooleanField { + return NewBooleanFieldWithIndexingOptions(name, arrayPositions, b, DefaultBooleanIndexingOptions) +} + +func NewBooleanFieldWithIndexingOptions(name string, arrayPositions []uint64, b bool, options index.FieldIndexingOptions) *BooleanField { + numPlainTextBytes := 5 + v := []byte("F") + if b { + numPlainTextBytes = 4 + v = []byte("T") + } + return &BooleanField{ + name: name, + arrayPositions: arrayPositions, + value: v, + options: options, + numPlainTextBytes: uint64(numPlainTextBytes), + } +} diff --git a/document/field_composite.go b/document/field_composite.go new file mode 100644 index 0000000..e0ba8af --- /dev/null +++ b/document/field_composite.go @@ -0,0 +1,138 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "reflect" + + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeCompositeField int + +func init() { + var cf CompositeField + reflectStaticSizeCompositeField = int(reflect.TypeOf(cf).Size()) +} + +const DefaultCompositeIndexingOptions = index.IndexField + +type CompositeField struct { + name string + includedFields map[string]bool + excludedFields map[string]bool + defaultInclude bool + options index.FieldIndexingOptions + totalLength int + compositeFrequencies index.TokenFrequencies +} + +func NewCompositeField(name string, defaultInclude bool, include []string, exclude []string) *CompositeField { + return NewCompositeFieldWithIndexingOptions(name, defaultInclude, include, exclude, DefaultCompositeIndexingOptions) +} + +func NewCompositeFieldWithIndexingOptions(name string, defaultInclude bool, include []string, exclude []string, options index.FieldIndexingOptions) *CompositeField { + rv := &CompositeField{ + name: name, + options: options, + defaultInclude: defaultInclude, + includedFields: make(map[string]bool, len(include)), + excludedFields: make(map[string]bool, len(exclude)), + compositeFrequencies: make(index.TokenFrequencies), + } + + for _, i := range include { + rv.includedFields[i] = true + } + for _, e := range exclude { + rv.excludedFields[e] = true + } + + return rv +} + +func (c *CompositeField) Size() int { + sizeInBytes := reflectStaticSizeCompositeField + size.SizeOfPtr + + len(c.name) + + for k := range c.includedFields { + sizeInBytes += size.SizeOfString + len(k) + size.SizeOfBool + } + + for k := range c.excludedFields { + sizeInBytes += size.SizeOfString + len(k) + size.SizeOfBool + } + if c.compositeFrequencies != nil { + sizeInBytes += c.compositeFrequencies.Size() + } + + return sizeInBytes +} + +func (c *CompositeField) Name() string { + return c.name +} + +func (c *CompositeField) ArrayPositions() []uint64 { + return []uint64{} +} + +func (c *CompositeField) Options() index.FieldIndexingOptions { + return c.options +} + +func (c *CompositeField) Analyze() { +} + +func (c *CompositeField) Value() []byte { + return []byte{} +} + +func (c *CompositeField) NumPlainTextBytes() uint64 { + return 0 +} + +func (c *CompositeField) includesField(field string) bool { + shouldInclude := c.defaultInclude + _, fieldShouldBeIncluded := c.includedFields[field] + if fieldShouldBeIncluded { + shouldInclude = true + } + _, fieldShouldBeExcluded := c.excludedFields[field] + if fieldShouldBeExcluded { + shouldInclude = false + } + return shouldInclude +} + +func (c *CompositeField) Compose(field string, length int, freq index.TokenFrequencies) { + if c.includesField(field) { + c.totalLength += length + c.compositeFrequencies.MergeAll(field, freq) + } +} + +func (c *CompositeField) EncodedFieldType() byte { + return 'c' +} + +func (c *CompositeField) AnalyzedLength() int { + return c.totalLength +} + +func (c *CompositeField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return c.compositeFrequencies +} diff --git a/document/field_datetime.go b/document/field_datetime.go new file mode 100644 index 0000000..f3b859c --- /dev/null +++ b/document/field_datetime.go @@ -0,0 +1,202 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "bytes" + "fmt" + "math" + "reflect" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var dateTimeValueSeperator = []byte{'\xff'} + +var reflectStaticSizeDateTimeField int + +func init() { + var f DateTimeField + reflectStaticSizeDateTimeField = int(reflect.TypeOf(f).Size()) +} + +const DefaultDateTimeIndexingOptions = index.StoreField | index.IndexField | index.DocValues +const DefaultDateTimePrecisionStep uint = 4 + +var MinTimeRepresentable = time.Unix(0, math.MinInt64) +var MaxTimeRepresentable = time.Unix(0, math.MaxInt64) + +type DateTimeField struct { + name string + arrayPositions []uint64 + options index.FieldIndexingOptions + value numeric.PrefixCoded + numPlainTextBytes uint64 + length int + frequencies index.TokenFrequencies +} + +func (n *DateTimeField) Size() int { + var freqSize int + if n.frequencies != nil { + freqSize = n.frequencies.Size() + } + return reflectStaticSizeDateTimeField + size.SizeOfPtr + + len(n.name) + + len(n.arrayPositions)*size.SizeOfUint64 + + len(n.value) + + freqSize +} + +func (n *DateTimeField) Name() string { + return n.name +} + +func (n *DateTimeField) ArrayPositions() []uint64 { + return n.arrayPositions +} + +func (n *DateTimeField) Options() index.FieldIndexingOptions { + return n.options +} + +func (n *DateTimeField) EncodedFieldType() byte { + return 'd' +} + +func (n *DateTimeField) AnalyzedLength() int { + return n.length +} + +func (n *DateTimeField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return n.frequencies +} + +// split the value into the prefix coded date and the layout +// using the dateTimeValueSeperator as the split point +func (n *DateTimeField) splitValue() (numeric.PrefixCoded, string) { + parts := bytes.SplitN(n.value, dateTimeValueSeperator, 2) + if len(parts) == 1 { + return numeric.PrefixCoded(parts[0]), "" + } + return numeric.PrefixCoded(parts[0]), string(parts[1]) +} + +func (n *DateTimeField) Analyze() { + valueWithoutLayout, _ := n.splitValue() + tokens := make(analysis.TokenStream, 0) + tokens = append(tokens, &analysis.Token{ + Start: 0, + End: len(valueWithoutLayout), + Term: valueWithoutLayout, + Position: 1, + Type: analysis.DateTime, + }) + + original, err := valueWithoutLayout.Int64() + if err == nil { + + shift := DefaultDateTimePrecisionStep + for shift < 64 { + shiftEncoded, err := numeric.NewPrefixCodedInt64(original, shift) + if err != nil { + break + } + token := analysis.Token{ + Start: 0, + End: len(shiftEncoded), + Term: shiftEncoded, + Position: 1, + Type: analysis.DateTime, + } + tokens = append(tokens, &token) + shift += DefaultDateTimePrecisionStep + } + } + + n.length = len(tokens) + n.frequencies = analysis.TokenFrequency(tokens, n.arrayPositions, n.options) +} + +func (n *DateTimeField) Value() []byte { + return n.value +} + +func (n *DateTimeField) DateTime() (time.Time, string, error) { + date, layout := n.splitValue() + i64, err := date.Int64() + if err != nil { + return time.Time{}, "", err + } + return time.Unix(0, i64).UTC(), layout, nil +} + +func (n *DateTimeField) GoString() string { + return fmt.Sprintf("&document.DateField{Name:%s, Options: %s, Value: %s}", n.name, n.options, n.value) +} + +func (n *DateTimeField) NumPlainTextBytes() uint64 { + return n.numPlainTextBytes +} + +func NewDateTimeFieldFromBytes(name string, arrayPositions []uint64, value []byte) *DateTimeField { + return &DateTimeField{ + name: name, + arrayPositions: arrayPositions, + value: value, + options: DefaultDateTimeIndexingOptions, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewDateTimeField(name string, arrayPositions []uint64, dt time.Time, layout string) (*DateTimeField, error) { + return NewDateTimeFieldWithIndexingOptions(name, arrayPositions, dt, layout, DefaultDateTimeIndexingOptions) +} + +func NewDateTimeFieldWithIndexingOptions(name string, arrayPositions []uint64, dt time.Time, layout string, options index.FieldIndexingOptions) (*DateTimeField, error) { + if canRepresent(dt) { + dtInt64 := dt.UnixNano() + prefixCoded := numeric.MustNewPrefixCodedInt64(dtInt64, 0) + // The prefixCoded value is combined with the layout. + // This is necessary because the storage layer stores a fields value as a byte slice + // without storing extra information like layout. So by making value = prefixCoded + layout, + // both pieces of information are stored in the byte slice. + // During a query, the layout is extracted from the byte slice stored to correctly + // format the prefixCoded value. + valueWithLayout := append(prefixCoded, dateTimeValueSeperator...) + valueWithLayout = append(valueWithLayout, []byte(layout)...) + return &DateTimeField{ + name: name, + arrayPositions: arrayPositions, + value: valueWithLayout, + options: options, + // not correct, just a place holder until we revisit how fields are + // represented and can fix this better + numPlainTextBytes: uint64(8), + }, nil + } + return nil, fmt.Errorf("cannot represent %s in this type", dt) +} + +func canRepresent(dt time.Time) bool { + if dt.Before(MinTimeRepresentable) || dt.After(MaxTimeRepresentable) { + return false + } + return true +} diff --git a/document/field_geopoint.go b/document/field_geopoint.go new file mode 100644 index 0000000..5795043 --- /dev/null +++ b/document/field_geopoint.go @@ -0,0 +1,199 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeGeoPointField int + +func init() { + var f GeoPointField + reflectStaticSizeGeoPointField = int(reflect.TypeOf(f).Size()) +} + +var GeoPrecisionStep uint = 9 + +type GeoPointField struct { + name string + arrayPositions []uint64 + options index.FieldIndexingOptions + value numeric.PrefixCoded + numPlainTextBytes uint64 + length int + frequencies index.TokenFrequencies + + spatialplugin index.SpatialAnalyzerPlugin +} + +func (n *GeoPointField) Size() int { + var freqSize int + if n.frequencies != nil { + freqSize = n.frequencies.Size() + } + return reflectStaticSizeGeoPointField + size.SizeOfPtr + + len(n.name) + + len(n.arrayPositions)*size.SizeOfUint64 + + len(n.value) + + freqSize +} + +func (n *GeoPointField) Name() string { + return n.name +} + +func (n *GeoPointField) ArrayPositions() []uint64 { + return n.arrayPositions +} + +func (n *GeoPointField) Options() index.FieldIndexingOptions { + return n.options +} + +func (n *GeoPointField) EncodedFieldType() byte { + return 'g' +} + +func (n *GeoPointField) AnalyzedLength() int { + return n.length +} + +func (n *GeoPointField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return n.frequencies +} + +func (n *GeoPointField) Analyze() { + tokens := make(analysis.TokenStream, 0, 8) + tokens = append(tokens, &analysis.Token{ + Start: 0, + End: len(n.value), + Term: n.value, + Position: 1, + Type: analysis.Numeric, + }) + + if n.spatialplugin != nil { + lat, _ := n.Lat() + lon, _ := n.Lon() + p := &geo.Point{Lat: lat, Lon: lon} + terms := n.spatialplugin.GetIndexTokens(p) + + for _, term := range terms { + token := analysis.Token{ + Start: 0, + End: len(term), + Term: []byte(term), + Position: 1, + Type: analysis.AlphaNumeric, + } + tokens = append(tokens, &token) + } + } else { + original, err := n.value.Int64() + if err == nil { + + shift := GeoPrecisionStep + for shift < 64 { + shiftEncoded, err := numeric.NewPrefixCodedInt64(original, shift) + if err != nil { + break + } + token := analysis.Token{ + Start: 0, + End: len(shiftEncoded), + Term: shiftEncoded, + Position: 1, + Type: analysis.Numeric, + } + tokens = append(tokens, &token) + shift += GeoPrecisionStep + } + } + } + + n.length = len(tokens) + n.frequencies = analysis.TokenFrequency(tokens, n.arrayPositions, n.options) +} + +func (n *GeoPointField) Value() []byte { + return n.value +} + +func (n *GeoPointField) Lon() (float64, error) { + i64, err := n.value.Int64() + if err != nil { + return 0.0, err + } + return geo.MortonUnhashLon(uint64(i64)), nil +} + +func (n *GeoPointField) Lat() (float64, error) { + i64, err := n.value.Int64() + if err != nil { + return 0.0, err + } + return geo.MortonUnhashLat(uint64(i64)), nil +} + +func (n *GeoPointField) GoString() string { + return fmt.Sprintf("&document.GeoPointField{Name:%s, Options: %s, Value: %s}", n.name, n.options, n.value) +} + +func (n *GeoPointField) NumPlainTextBytes() uint64 { + return n.numPlainTextBytes +} + +func NewGeoPointFieldFromBytes(name string, arrayPositions []uint64, value []byte) *GeoPointField { + return &GeoPointField{ + name: name, + arrayPositions: arrayPositions, + value: value, + options: DefaultNumericIndexingOptions, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewGeoPointField(name string, arrayPositions []uint64, lon, lat float64) *GeoPointField { + return NewGeoPointFieldWithIndexingOptions(name, arrayPositions, lon, lat, DefaultNumericIndexingOptions) +} + +func NewGeoPointFieldWithIndexingOptions(name string, arrayPositions []uint64, lon, lat float64, options index.FieldIndexingOptions) *GeoPointField { + mhash := geo.MortonHash(lon, lat) + prefixCoded := numeric.MustNewPrefixCodedInt64(int64(mhash), 0) + return &GeoPointField{ + name: name, + arrayPositions: arrayPositions, + value: prefixCoded, + options: options, + // not correct, just a place holder until we revisit how fields are + // represented and can fix this better + numPlainTextBytes: uint64(8), + } +} + +// SetSpatialAnalyzerPlugin implements the +// index.TokenisableSpatialField interface. +func (n *GeoPointField) SetSpatialAnalyzerPlugin( + plugin index.SpatialAnalyzerPlugin) { + n.spatialplugin = plugin +} diff --git a/document/field_geopoint_test.go b/document/field_geopoint_test.go new file mode 100644 index 0000000..2860264 --- /dev/null +++ b/document/field_geopoint_test.go @@ -0,0 +1,16 @@ +package document + +import "testing" + +func TestGeoPointField(t *testing.T) { + gf := NewGeoPointField("loc", []uint64{}, 0.0015, 0.0015) + gf.Analyze() + numTokens := gf.AnalyzedLength() + tokenFreqs := gf.AnalyzedTokenFrequencies() + if numTokens != 8 { + t.Errorf("expected 8 tokens, got %d", numTokens) + } + if len(tokenFreqs) != 8 { + t.Errorf("expected 8 token freqs") + } +} diff --git a/document/field_geoshape.go b/document/field_geoshape.go new file mode 100644 index 0000000..aa73c29 --- /dev/null +++ b/document/field_geoshape.go @@ -0,0 +1,265 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" + "github.com/blevesearch/geo/geojson" +) + +var reflectStaticSizeGeoShapeField int + +func init() { + var f GeoShapeField + reflectStaticSizeGeoShapeField = int(reflect.TypeOf(f).Size()) +} + +const DefaultGeoShapeIndexingOptions = index.IndexField | index.DocValues + +type GeoShapeField struct { + name string + shape index.GeoJSON + arrayPositions []uint64 + options index.FieldIndexingOptions + numPlainTextBytes uint64 + length int + encodedValue []byte + value []byte + + frequencies index.TokenFrequencies +} + +func (n *GeoShapeField) Size() int { + var freqSize int + if n.frequencies != nil { + freqSize = n.frequencies.Size() + } + return reflectStaticSizeGeoShapeField + size.SizeOfPtr + + len(n.name) + + len(n.arrayPositions)*size.SizeOfUint64 + + len(n.encodedValue) + + len(n.value) + + freqSize +} + +func (n *GeoShapeField) Name() string { + return n.name +} + +func (n *GeoShapeField) ArrayPositions() []uint64 { + return n.arrayPositions +} + +func (n *GeoShapeField) Options() index.FieldIndexingOptions { + return n.options +} + +func (n *GeoShapeField) EncodedFieldType() byte { + return 's' +} + +func (n *GeoShapeField) AnalyzedLength() int { + return n.length +} + +func (n *GeoShapeField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return n.frequencies +} + +func (n *GeoShapeField) Analyze() { + // compute the bytes representation for the coordinates + tokens := make(analysis.TokenStream, 0) + + rti := geo.GetSpatialAnalyzerPlugin("s2") + terms := rti.GetIndexTokens(n.shape) + + for _, term := range terms { + token := analysis.Token{ + Start: 0, + End: len(term), + Term: []byte(term), + Position: 1, + Type: analysis.AlphaNumeric, + } + tokens = append(tokens, &token) + } + + n.length = len(tokens) + n.frequencies = analysis.TokenFrequency(tokens, n.arrayPositions, n.options) +} + +func (n *GeoShapeField) Value() []byte { + return n.value +} + +func (n *GeoShapeField) GoString() string { + return fmt.Sprintf("&document.GeoShapeField{Name:%s, Options: %s, Value: %s}", + n.name, n.options, n.value) +} + +func (n *GeoShapeField) NumPlainTextBytes() uint64 { + return n.numPlainTextBytes +} + +func (n *GeoShapeField) EncodedShape() []byte { + return n.encodedValue +} + +func NewGeoShapeField(name string, arrayPositions []uint64, + coordinates [][][][]float64, typ string) *GeoShapeField { + return NewGeoShapeFieldWithIndexingOptions(name, arrayPositions, + coordinates, typ, DefaultGeoShapeIndexingOptions) +} + +func NewGeoShapeFieldFromBytes(name string, arrayPositions []uint64, + value []byte) *GeoShapeField { + return &GeoShapeField{ + name: name, + arrayPositions: arrayPositions, + value: value, + options: DefaultGeoShapeIndexingOptions, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewGeoShapeFieldWithIndexingOptions(name string, arrayPositions []uint64, + coordinates [][][][]float64, typ string, + options index.FieldIndexingOptions) *GeoShapeField { + shape := &geojson.GeoShape{ + Coordinates: coordinates, + Type: typ, + } + + return NewGeoShapeFieldFromShapeWithIndexingOptions(name, + arrayPositions, shape, options) +} + +func NewGeoShapeFieldFromShapeWithIndexingOptions(name string, arrayPositions []uint64, + geoShape *geojson.GeoShape, options index.FieldIndexingOptions) *GeoShapeField { + + var shape index.GeoJSON + var encodedValue []byte + var err error + + if geoShape.Type == geo.CircleType { + shape, encodedValue, err = geo.NewGeoCircleShape(geoShape.Center, geoShape.Radius) + } else { + shape, encodedValue, err = geo.NewGeoJsonShape(geoShape.Coordinates, geoShape.Type) + } + if err != nil { + return nil + } + + // extra glue bytes to work around the term splitting logic from interfering + // the custom encoding of the geoshape coordinates inside the docvalues. + encodedValue = append(geo.GlueBytes, append(encodedValue, geo.GlueBytes...)...) + + // get the byte value for the geoshape. + value, err := shape.Value() + if err != nil { + return nil + } + + // docvalues are always enabled for geoshape fields, even if the + // indexing options are set to not include docvalues. + options = options | index.DocValues + + return &GeoShapeField{ + shape: shape, + name: name, + arrayPositions: arrayPositions, + options: options, + encodedValue: encodedValue, + value: value, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewGeometryCollectionFieldWithIndexingOptions(name string, + arrayPositions []uint64, coordinates [][][][][]float64, types []string, + options index.FieldIndexingOptions) *GeoShapeField { + if len(coordinates) != len(types) { + return nil + } + + shapes := make([]*geojson.GeoShape, len(types)) + for i := range coordinates { + shapes[i] = &geojson.GeoShape{ + Coordinates: coordinates[i], + Type: types[i], + } + } + + return NewGeometryCollectionFieldFromShapesWithIndexingOptions(name, + arrayPositions, shapes, options) +} + +func NewGeometryCollectionFieldFromShapesWithIndexingOptions(name string, + arrayPositions []uint64, geoShapes []*geojson.GeoShape, + options index.FieldIndexingOptions) *GeoShapeField { + shape, encodedValue, err := geo.NewGeometryCollectionFromShapes(geoShapes) + if err != nil { + return nil + } + + // extra glue bytes to work around the term splitting logic from interfering + // the custom encoding of the geoshape coordinates inside the docvalues. + encodedValue = append(geo.GlueBytes, append(encodedValue, geo.GlueBytes...)...) + + // get the byte value for the geometryCollection. + value, err := shape.Value() + if err != nil { + return nil + } + + // docvalues are always enabled for geoshape fields, even if the + // indexing options are set to not include docvalues. + options = options | index.DocValues + + return &GeoShapeField{ + shape: shape, + name: name, + arrayPositions: arrayPositions, + options: options, + encodedValue: encodedValue, + value: value, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewGeoCircleFieldWithIndexingOptions(name string, arrayPositions []uint64, + centerPoint []float64, radius string, + options index.FieldIndexingOptions) *GeoShapeField { + + shape := &geojson.GeoShape{ + Center: centerPoint, + Radius: radius, + Type: geo.CircleType, + } + + return NewGeoShapeFieldFromShapeWithIndexingOptions(name, + arrayPositions, shape, options) +} + +// GeoShape is an implementation of the index.GeoShapeField interface. +func (n *GeoShapeField) GeoShape() (index.GeoJSON, error) { + return geojson.ParseGeoJSONShape(n.value) +} diff --git a/document/field_ip.go b/document/field_ip.go new file mode 100644 index 0000000..3a5ab37 --- /dev/null +++ b/document/field_ip.go @@ -0,0 +1,137 @@ +// Copyright (c) 2021 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "fmt" + "net" + "reflect" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeIPField int + +func init() { + var f IPField + reflectStaticSizeIPField = int(reflect.TypeOf(f).Size()) +} + +const DefaultIPIndexingOptions = index.StoreField | index.IndexField | index.DocValues + +type IPField struct { + name string + arrayPositions []uint64 + options index.FieldIndexingOptions + value net.IP + numPlainTextBytes uint64 + length int + frequencies index.TokenFrequencies +} + +func (b *IPField) Size() int { + var freqSize int + if b.frequencies != nil { + freqSize = b.frequencies.Size() + } + return reflectStaticSizeIPField + size.SizeOfPtr + + len(b.name) + + len(b.arrayPositions)*size.SizeOfUint64 + + len(b.value) + + freqSize +} + +func (b *IPField) Name() string { + return b.name +} + +func (b *IPField) ArrayPositions() []uint64 { + return b.arrayPositions +} + +func (b *IPField) Options() index.FieldIndexingOptions { + return b.options +} + +func (n *IPField) EncodedFieldType() byte { + return 'i' +} + +func (n *IPField) AnalyzedLength() int { + return n.length +} + +func (n *IPField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return n.frequencies +} + +func (b *IPField) Analyze() { + + tokens := analysis.TokenStream{ + &analysis.Token{ + Start: 0, + End: len(b.value), + Term: b.value, + Position: 1, + Type: analysis.IP, + }, + } + b.length = 1 + b.frequencies = analysis.TokenFrequency(tokens, b.arrayPositions, b.options) +} + +func (b *IPField) Value() []byte { + return b.value +} + +func (b *IPField) IP() (net.IP, error) { + return net.IP(b.value), nil +} + +func (b *IPField) GoString() string { + return fmt.Sprintf("&document.IPField{Name:%s, Options: %s, Value: %s}", b.name, b.options, net.IP(b.value)) +} + +func (b *IPField) NumPlainTextBytes() uint64 { + return b.numPlainTextBytes +} + +func NewIPFieldFromBytes(name string, arrayPositions []uint64, value []byte) *IPField { + return &IPField{ + name: name, + arrayPositions: arrayPositions, + value: value, + options: DefaultIPIndexingOptions, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewIPField(name string, arrayPositions []uint64, v net.IP) *IPField { + return NewIPFieldWithIndexingOptions(name, arrayPositions, v, DefaultIPIndexingOptions) +} + +func NewIPFieldWithIndexingOptions(name string, arrayPositions []uint64, b net.IP, options index.FieldIndexingOptions) *IPField { + v := b.To16() + + return &IPField{ + name: name, + arrayPositions: arrayPositions, + value: v, + options: options, + numPlainTextBytes: net.IPv6len, + } +} diff --git a/document/field_ip_test.go b/document/field_ip_test.go new file mode 100644 index 0000000..e5e8bf2 --- /dev/null +++ b/document/field_ip_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2021 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "bytes" + "net" + "testing" +) + +func TestIPField(t *testing.T) { + nf := NewIPField("ip", []uint64{}, net.IPv4(192, 168, 1, 1)) + nf.Analyze() + if nf.length != 1 { + t.Errorf("expected 1 token") + } + if len(nf.value) != 16 { + t.Errorf("stored value should be in 16 byte ipv6 format") + } + if !bytes.Equal(nf.value, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 1, 1}) { + t.Errorf("wrong value stored, expected 192.168.1.1, got %q", nf.value.String()) + } + if len(nf.frequencies) != 1 { + t.Errorf("expected 1 token freqs") + } +} diff --git a/document/field_numeric.go b/document/field_numeric.go new file mode 100644 index 0000000..1ee7b75 --- /dev/null +++ b/document/field_numeric.go @@ -0,0 +1,165 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeNumericField int + +func init() { + var f NumericField + reflectStaticSizeNumericField = int(reflect.TypeOf(f).Size()) +} + +const DefaultNumericIndexingOptions = index.StoreField | index.IndexField | index.DocValues + +const DefaultPrecisionStep uint = 4 + +type NumericField struct { + name string + arrayPositions []uint64 + options index.FieldIndexingOptions + value numeric.PrefixCoded + numPlainTextBytes uint64 + length int + frequencies index.TokenFrequencies +} + +func (n *NumericField) Size() int { + var freqSize int + if n.frequencies != nil { + freqSize = n.frequencies.Size() + } + return reflectStaticSizeNumericField + size.SizeOfPtr + + len(n.name) + + len(n.arrayPositions)*size.SizeOfUint64 + + len(n.value) + + freqSize +} + +func (n *NumericField) Name() string { + return n.name +} + +func (n *NumericField) ArrayPositions() []uint64 { + return n.arrayPositions +} + +func (n *NumericField) Options() index.FieldIndexingOptions { + return n.options +} + +func (n *NumericField) EncodedFieldType() byte { + return 'n' +} + +func (n *NumericField) AnalyzedLength() int { + return n.length +} + +func (n *NumericField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return n.frequencies +} + +func (n *NumericField) Analyze() { + tokens := make(analysis.TokenStream, 0) + tokens = append(tokens, &analysis.Token{ + Start: 0, + End: len(n.value), + Term: n.value, + Position: 1, + Type: analysis.Numeric, + }) + + original, err := n.value.Int64() + if err == nil { + + shift := DefaultPrecisionStep + for shift < 64 { + shiftEncoded, err := numeric.NewPrefixCodedInt64(original, shift) + if err != nil { + break + } + token := analysis.Token{ + Start: 0, + End: len(shiftEncoded), + Term: shiftEncoded, + Position: 1, + Type: analysis.Numeric, + } + tokens = append(tokens, &token) + shift += DefaultPrecisionStep + } + } + + n.length = len(tokens) + n.frequencies = analysis.TokenFrequency(tokens, n.arrayPositions, n.options) +} + +func (n *NumericField) Value() []byte { + return n.value +} + +func (n *NumericField) Number() (float64, error) { + i64, err := n.value.Int64() + if err != nil { + return 0.0, err + } + return numeric.Int64ToFloat64(i64), nil +} + +func (n *NumericField) GoString() string { + return fmt.Sprintf("&document.NumericField{Name:%s, Options: %s, Value: %s}", n.name, n.options, n.value) +} + +func (n *NumericField) NumPlainTextBytes() uint64 { + return n.numPlainTextBytes +} + +func NewNumericFieldFromBytes(name string, arrayPositions []uint64, value []byte) *NumericField { + return &NumericField{ + name: name, + arrayPositions: arrayPositions, + value: value, + options: DefaultNumericIndexingOptions, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewNumericField(name string, arrayPositions []uint64, number float64) *NumericField { + return NewNumericFieldWithIndexingOptions(name, arrayPositions, number, DefaultNumericIndexingOptions) +} + +func NewNumericFieldWithIndexingOptions(name string, arrayPositions []uint64, number float64, options index.FieldIndexingOptions) *NumericField { + numberInt64 := numeric.Float64ToInt64(number) + prefixCoded := numeric.MustNewPrefixCodedInt64(numberInt64, 0) + return &NumericField{ + name: name, + arrayPositions: arrayPositions, + value: prefixCoded, + options: options, + // not correct, just a place holder until we revisit how fields are + // represented and can fix this better + numPlainTextBytes: uint64(8), + } +} diff --git a/document/field_numeric_test.go b/document/field_numeric_test.go new file mode 100644 index 0000000..5f36d68 --- /dev/null +++ b/document/field_numeric_test.go @@ -0,0 +1,32 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "testing" +) + +func TestNumericField(t *testing.T) { + nf := NewNumericField("age", []uint64{}, 3.4) + nf.Analyze() + numTokens := nf.AnalyzedLength() + tokenFreqs := nf.AnalyzedTokenFrequencies() + if numTokens != 16 { + t.Errorf("expected 16 tokens") + } + if len(tokenFreqs) != 16 { + t.Errorf("expected 16 token freqs") + } +} diff --git a/document/field_synonym.go b/document/field_synonym.go new file mode 100644 index 0000000..c34b481 --- /dev/null +++ b/document/field_synonym.go @@ -0,0 +1,149 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "reflect" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeSynonymField int + +func init() { + var f SynonymField + reflectStaticSizeSynonymField = int(reflect.TypeOf(f).Size()) +} + +const DefaultSynonymIndexingOptions = index.IndexField + +type SynonymField struct { + name string + analyzer analysis.Analyzer + options index.FieldIndexingOptions + input []string + synonyms []string + numPlainTextBytes uint64 + + // populated during analysis + synonymMap map[string][]string +} + +func (s *SynonymField) Size() int { + return reflectStaticSizeSynonymField + size.SizeOfPtr + + len(s.name) +} + +func (s *SynonymField) Name() string { + return s.name +} + +func (s *SynonymField) ArrayPositions() []uint64 { + return nil +} + +func (s *SynonymField) Options() index.FieldIndexingOptions { + return s.options +} + +func (s *SynonymField) NumPlainTextBytes() uint64 { + return s.numPlainTextBytes +} + +func (s *SynonymField) AnalyzedLength() int { + return 0 +} + +func (s *SynonymField) EncodedFieldType() byte { + return 'y' +} + +func (s *SynonymField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return nil +} + +func (s *SynonymField) Analyze() { + var analyzedInput []string + if len(s.input) > 0 { + analyzedInput = make([]string, 0, len(s.input)) + for _, term := range s.input { + analyzedTerm := analyzeSynonymTerm(term, s.analyzer) + if analyzedTerm != "" { + analyzedInput = append(analyzedInput, analyzedTerm) + } + } + } + analyzedSynonyms := make([]string, 0, len(s.synonyms)) + for _, syn := range s.synonyms { + analyzedTerm := analyzeSynonymTerm(syn, s.analyzer) + if analyzedTerm != "" { + analyzedSynonyms = append(analyzedSynonyms, analyzedTerm) + } + } + s.synonymMap = processSynonymData(analyzedInput, analyzedSynonyms) +} + +func (s *SynonymField) Value() []byte { + return nil +} + +func (s *SynonymField) IterateSynonyms(visitor func(term string, synonyms []string)) { + for term, synonyms := range s.synonymMap { + visitor(term, synonyms) + } +} + +func NewSynonymField(name string, analyzer analysis.Analyzer, input []string, synonyms []string) *SynonymField { + return &SynonymField{ + name: name, + analyzer: analyzer, + options: DefaultSynonymIndexingOptions, + input: input, + synonyms: synonyms, + } +} + +func processSynonymData(input []string, synonyms []string) map[string][]string { + var synonymMap map[string][]string + if len(input) > 0 { + // Map each term to the same list of synonyms. + synonymMap = make(map[string][]string, len(input)) + for _, term := range input { + synonymMap[term] = synonyms + } + } else { + synonymMap = make(map[string][]string, len(synonyms)) + // Precompute a map where each synonym points to all other synonyms. + for i, elem := range synonyms { + synonymMap[elem] = make([]string, 0, len(synonyms)-1) + for j, otherElem := range synonyms { + if i != j { + synonymMap[elem] = append(synonymMap[elem], otherElem) + } + } + } + } + return synonymMap +} + +func analyzeSynonymTerm(term string, analyzer analysis.Analyzer) string { + tokenStream := analyzer.Analyze([]byte(term)) + if len(tokenStream) == 1 { + return string(tokenStream[0].Term) + } + return "" +} diff --git a/document/field_text.go b/document/field_text.go new file mode 100644 index 0000000..d35e747 --- /dev/null +++ b/document/field_text.go @@ -0,0 +1,162 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package document + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeTextField int + +func init() { + var f TextField + reflectStaticSizeTextField = int(reflect.TypeOf(f).Size()) +} + +const DefaultTextIndexingOptions = index.IndexField | index.DocValues + +type TextField struct { + name string + arrayPositions []uint64 + options index.FieldIndexingOptions + analyzer analysis.Analyzer + value []byte + numPlainTextBytes uint64 + length int + frequencies index.TokenFrequencies +} + +func (t *TextField) Size() int { + var freqSize int + if t.frequencies != nil { + freqSize = t.frequencies.Size() + } + return reflectStaticSizeTextField + size.SizeOfPtr + + len(t.name) + + len(t.arrayPositions)*size.SizeOfUint64 + + len(t.value) + + freqSize +} + +func (t *TextField) Name() string { + return t.name +} + +func (t *TextField) ArrayPositions() []uint64 { + return t.arrayPositions +} + +func (t *TextField) Options() index.FieldIndexingOptions { + return t.options +} + +func (t *TextField) EncodedFieldType() byte { + return 't' +} + +func (t *TextField) AnalyzedLength() int { + return t.length +} + +func (t *TextField) AnalyzedTokenFrequencies() index.TokenFrequencies { + return t.frequencies +} + +func (t *TextField) Analyze() { + var tokens analysis.TokenStream + if t.analyzer != nil { + bytesToAnalyze := t.Value() + if t.options.IsStored() { + // need to copy + bytesCopied := make([]byte, len(bytesToAnalyze)) + copy(bytesCopied, bytesToAnalyze) + bytesToAnalyze = bytesCopied + } + tokens = t.analyzer.Analyze(bytesToAnalyze) + } else { + tokens = analysis.TokenStream{ + &analysis.Token{ + Start: 0, + End: len(t.value), + Term: t.value, + Position: 1, + Type: analysis.AlphaNumeric, + }, + } + } + t.length = len(tokens) // number of tokens in this doc field + t.frequencies = analysis.TokenFrequency(tokens, t.arrayPositions, t.options) +} + +func (t *TextField) Analyzer() analysis.Analyzer { + return t.analyzer +} + +func (t *TextField) Value() []byte { + return t.value +} + +func (t *TextField) Text() string { + return string(t.value) +} + +func (t *TextField) GoString() string { + return fmt.Sprintf("&document.TextField{Name:%s, Options: %s, Analyzer: %v, Value: %s, ArrayPositions: %v}", t.name, t.options, t.analyzer, t.value, t.arrayPositions) +} + +func (t *TextField) NumPlainTextBytes() uint64 { + return t.numPlainTextBytes +} + +func NewTextField(name string, arrayPositions []uint64, value []byte) *TextField { + return NewTextFieldWithIndexingOptions(name, arrayPositions, value, DefaultTextIndexingOptions) +} + +func NewTextFieldWithIndexingOptions(name string, arrayPositions []uint64, value []byte, options index.FieldIndexingOptions) *TextField { + return &TextField{ + name: name, + arrayPositions: arrayPositions, + options: options, + value: value, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewTextFieldWithAnalyzer(name string, arrayPositions []uint64, value []byte, analyzer analysis.Analyzer) *TextField { + return &TextField{ + name: name, + arrayPositions: arrayPositions, + options: DefaultTextIndexingOptions, + analyzer: analyzer, + value: value, + numPlainTextBytes: uint64(len(value)), + } +} + +func NewTextFieldCustom(name string, arrayPositions []uint64, value []byte, options index.FieldIndexingOptions, analyzer analysis.Analyzer) *TextField { + return &TextField{ + name: name, + arrayPositions: arrayPositions, + options: options, + analyzer: analyzer, + value: value, + numPlainTextBytes: uint64(len(value)), + } +} diff --git a/document/field_vector.go b/document/field_vector.go new file mode 100644 index 0000000..9c55cf1 --- /dev/null +++ b/document/field_vector.go @@ -0,0 +1,146 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package document + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeVectorField int + +func init() { + var f VectorField + reflectStaticSizeVectorField = int(reflect.TypeOf(f).Size()) +} + +const DefaultVectorIndexingOptions = index.IndexField + +type VectorField struct { + name string + dims int // Dimensionality of the vector + similarity string // Similarity metric to use for scoring + options index.FieldIndexingOptions + value []float32 + numPlainTextBytes uint64 + vectorIndexOptimizedFor string // Optimization applied to this index. +} + +func (n *VectorField) Size() int { + return reflectStaticSizeVectorField + size.SizeOfPtr + + len(n.name) + + len(n.similarity) + + len(n.vectorIndexOptimizedFor) + + int(numBytesFloat32s(n.value)) +} + +func (n *VectorField) Name() string { + return n.name +} + +func (n *VectorField) ArrayPositions() []uint64 { + return nil +} + +func (n *VectorField) Options() index.FieldIndexingOptions { + return n.options +} + +func (n *VectorField) NumPlainTextBytes() uint64 { + return n.numPlainTextBytes +} + +func (n *VectorField) AnalyzedLength() int { + // vectors aren't analyzed + return 0 +} + +func (n *VectorField) EncodedFieldType() byte { + return 'v' +} + +func (n *VectorField) AnalyzedTokenFrequencies() index.TokenFrequencies { + // vectors aren't analyzed + return nil +} + +func (n *VectorField) Analyze() { + // vectors aren't analyzed +} + +func (n *VectorField) Value() []byte { + return nil +} + +func (n *VectorField) GoString() string { + return fmt.Sprintf("&document.VectorField{Name:%s, Options: %s, "+ + "Value: %+v}", n.name, n.options, n.value) +} + +// For the sake of not polluting the API, we are keeping arrayPositions as a +// parameter, but it is not used. +func NewVectorField(name string, arrayPositions []uint64, + vector []float32, dims int, similarity, vectorIndexOptimizedFor string) *VectorField { + return NewVectorFieldWithIndexingOptions(name, arrayPositions, + vector, dims, similarity, vectorIndexOptimizedFor, + DefaultVectorIndexingOptions) +} + +// For the sake of not polluting the API, we are keeping arrayPositions as a +// parameter, but it is not used. +func NewVectorFieldWithIndexingOptions(name string, arrayPositions []uint64, + vector []float32, dims int, similarity, vectorIndexOptimizedFor string, + options index.FieldIndexingOptions) *VectorField { + + return &VectorField{ + name: name, + dims: dims, + similarity: similarity, + options: options, + value: vector, + numPlainTextBytes: numBytesFloat32s(vector), + vectorIndexOptimizedFor: vectorIndexOptimizedFor, + } +} + +func numBytesFloat32s(value []float32) uint64 { + return uint64(len(value) * size.SizeOfFloat32) +} + +// ----------------------------------------------------------------------------- +// Following methods help in implementing the bleve_index_api's VectorField +// interface. + +func (n *VectorField) Vector() []float32 { + return n.value +} + +func (n *VectorField) Dims() int { + return n.dims +} + +func (n *VectorField) Similarity() string { + return n.similarity +} + +func (n *VectorField) IndexOptimizedFor() string { + return n.vectorIndexOptimizedFor +} diff --git a/document/field_vector_base64.go b/document/field_vector_base64.go new file mode 100644 index 0000000..31d6cbf --- /dev/null +++ b/document/field_vector_base64.go @@ -0,0 +1,163 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package document + +import ( + "encoding/base64" + "encoding/binary" + "fmt" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/size" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeVectorBase64Field int + +func init() { + var f VectorBase64Field + reflectStaticSizeVectorBase64Field = int(reflect.TypeOf(f).Size()) +} + +type VectorBase64Field struct { + vectorField *VectorField + base64Encoding string +} + +func (n *VectorBase64Field) Size() int { + var vecFieldSize int + if n.vectorField != nil { + vecFieldSize = n.vectorField.Size() + } + return reflectStaticSizeVectorBase64Field + size.SizeOfPtr + + len(n.base64Encoding) + + vecFieldSize +} + +func (n *VectorBase64Field) Name() string { + return n.vectorField.Name() +} + +func (n *VectorBase64Field) ArrayPositions() []uint64 { + return n.vectorField.ArrayPositions() +} + +func (n *VectorBase64Field) Options() index.FieldIndexingOptions { + return n.vectorField.Options() +} + +func (n *VectorBase64Field) NumPlainTextBytes() uint64 { + return n.vectorField.NumPlainTextBytes() +} + +func (n *VectorBase64Field) AnalyzedLength() int { + return n.vectorField.AnalyzedLength() +} + +func (n *VectorBase64Field) EncodedFieldType() byte { + return 'e' +} + +func (n *VectorBase64Field) AnalyzedTokenFrequencies() index.TokenFrequencies { + return n.vectorField.AnalyzedTokenFrequencies() +} + +func (n *VectorBase64Field) Analyze() { +} + +func (n *VectorBase64Field) Value() []byte { + return n.vectorField.Value() +} + +func (n *VectorBase64Field) GoString() string { + return fmt.Sprintf("&document.vectorFieldBase64Field{Name:%s, Options: %s, "+ + "Value: %+v}", n.vectorField.Name(), n.vectorField.Options(), n.vectorField.Value()) +} + +// For the sake of not polluting the API, we are keeping arrayPositions as a +// parameter, but it is not used. +func NewVectorBase64Field(name string, arrayPositions []uint64, vectorBase64 string, + dims int, similarity, vectorIndexOptimizedFor string) (*VectorBase64Field, error) { + + decodedVector, err := DecodeVector(vectorBase64) + if err != nil { + return nil, err + } + + return &VectorBase64Field{ + vectorField: NewVectorFieldWithIndexingOptions(name, arrayPositions, + decodedVector, dims, similarity, + vectorIndexOptimizedFor, DefaultVectorIndexingOptions), + + base64Encoding: vectorBase64, + }, nil +} + +// This function takes a base64 encoded string and decodes it into +// a vector. +func DecodeVector(encodedValue string) ([]float32, error) { + // We first decode the encoded string into a byte array. + decodedString, err := base64.StdEncoding.DecodeString(encodedValue) + if err != nil { + return nil, err + } + + // The array is expected to be divisible by 4 because each float32 + // should occupy 4 bytes + if len(decodedString)%size.SizeOfFloat32 != 0 { + return nil, fmt.Errorf("decoded byte array not divisible by %d", size.SizeOfFloat32) + } + dims := int(len(decodedString) / size.SizeOfFloat32) + + if dims <= 0 { + return nil, fmt.Errorf("unable to decode encoded vector") + } + + decodedVector := make([]float32, dims) + + // We iterate through the array 4 bytes at a time and convert each of + // them to a float32 value by reading them in a little endian notation + for i := 0; i < dims; i++ { + bytes := decodedString[i*size.SizeOfFloat32 : (i+1)*size.SizeOfFloat32] + entry := math.Float32frombits(binary.LittleEndian.Uint32(bytes)) + if !util.IsValidFloat32(float64(entry)) { + return nil, fmt.Errorf("invalid float32 value: %f", entry) + } + decodedVector[i] = entry + } + + return decodedVector, nil +} + +func (n *VectorBase64Field) Vector() []float32 { + return n.vectorField.Vector() +} + +func (n *VectorBase64Field) Dims() int { + return n.vectorField.Dims() +} + +func (n *VectorBase64Field) Similarity() string { + return n.vectorField.Similarity() +} + +func (n *VectorBase64Field) IndexOptimizedFor() string { + return n.vectorField.IndexOptimizedFor() +} diff --git a/document/field_vector_base64_test.go b/document/field_vector_base64_test.go new file mode 100644 index 0000000..91212b0 --- /dev/null +++ b/document/field_vector_base64_test.go @@ -0,0 +1,112 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package document + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "fmt" + "math/rand" + "testing" +) + +func TestDecodeVector(t *testing.T) { + vec := make([]float32, 2048) + for i := range vec { + vec[i] = rand.Float32() + } + + vecBytes := bytifyVec(vec) + encodedVec := base64.StdEncoding.EncodeToString(vecBytes) + + decodedVector, err := DecodeVector(encodedVec) + if err != nil { + t.Error(err) + } + if len(decodedVector) != len(vec) { + t.Errorf("Decoded vector dimensions not same as original vector dimensions") + } + + for i := range vec { + if vec[i] != decodedVector[i] { + t.Fatalf("Decoded vector not the same as original vector %v != %v", vec[i], decodedVector[i]) + } + } +} + +func BenchmarkDecodeVector128(b *testing.B) { + vec := make([]float32, 128) + for i := range vec { + vec[i] = rand.Float32() + } + + vecBytes := bytifyVec(vec) + encodedVec := base64.StdEncoding.EncodeToString(vecBytes) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = DecodeVector(encodedVec) + } +} + +func BenchmarkDecodeVector784(b *testing.B) { + vec := make([]float32, 784) + for i := range vec { + vec[i] = rand.Float32() + } + + vecBytes := bytifyVec(vec) + encodedVec := base64.StdEncoding.EncodeToString(vecBytes) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = DecodeVector(encodedVec) + } +} + +func BenchmarkDecodeVector1536(b *testing.B) { + vec := make([]float32, 1536) + for i := range vec { + vec[i] = rand.Float32() + } + + vecBytes := bytifyVec(vec) + encodedVec := base64.StdEncoding.EncodeToString(vecBytes) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = DecodeVector(encodedVec) + } +} + +func bytifyVec(vec []float32) []byte { + buf := new(bytes.Buffer) + + for _, v := range vec { + err := binary.Write(buf, binary.LittleEndian, v) + if err != nil { + fmt.Println(err) + } + } + + return buf.Bytes() +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..b57a615 --- /dev/null +++ b/error.go @@ -0,0 +1,54 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +// Constant Error values which can be compared to determine the type of error +const ( + ErrorIndexPathExists Error = iota + ErrorIndexPathDoesNotExist + ErrorIndexMetaMissing + ErrorIndexMetaCorrupt + ErrorIndexClosed + ErrorAliasMulti + ErrorAliasEmpty + ErrorUnknownIndexType + ErrorEmptyID + ErrorIndexReadInconsistency + ErrorTwoPhaseSearchInconsistency + ErrorSynonymSearchNotSupported +) + +// Error represents a more strongly typed bleve error for detecting +// and handling specific types of errors. +type Error int + +func (e Error) Error() string { + return errorMessages[e] +} + +var errorMessages = map[Error]string{ + ErrorIndexPathExists: "cannot create new index, path already exists", + ErrorIndexPathDoesNotExist: "cannot open index, path does not exist", + ErrorIndexMetaMissing: "cannot open index, metadata missing", + ErrorIndexMetaCorrupt: "cannot open index, metadata corrupt", + ErrorIndexClosed: "index is closed", + ErrorAliasMulti: "cannot perform single index operation on multiple index alias", + ErrorAliasEmpty: "cannot perform operation on empty alias", + ErrorUnknownIndexType: "unknown index type", + ErrorEmptyID: "document ID cannot be empty", + ErrorIndexReadInconsistency: "index read inconsistency detected", + ErrorTwoPhaseSearchInconsistency: "2-phase search failed, likely due to an overlapping topology change", + ErrorSynonymSearchNotSupported: "synonym search not supported", +} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..66d12bd --- /dev/null +++ b/examples_test.go @@ -0,0 +1,468 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight/highlighter/ansi" +) + +var indexMapping mapping.IndexMapping +var exampleIndex Index +var err error + +func TestMain(m *testing.M) { + err = os.RemoveAll("path_to_index") + if err != nil { + panic(err) + } + toRun := m.Run() + if exampleIndex != nil { + err = exampleIndex.Close() + if err != nil { + panic(err) + } + } + err = os.RemoveAll("path_to_index") + if err != nil { + panic(err) + } + os.Exit(toRun) +} + +func ExampleNew() { + indexMapping = NewIndexMapping() + exampleIndex, err = New("path_to_index", indexMapping) + if err != nil { + panic(err) + } + count, err := exampleIndex.DocCount() + if err != nil { + panic(err) + } + + fmt.Println(count) + // Output: + // 0 +} + +func ExampleIndex_indexing() { + data := struct { + Name string + Created time.Time + Age int + }{Name: "named one", Created: time.Now(), Age: 50} + data2 := struct { + Name string + Created time.Time + Age int + }{Name: "great nameless one", Created: time.Now(), Age: 25} + + // index some data + err = exampleIndex.Index("document id 1", data) + if err != nil { + panic(err) + } + err = exampleIndex.Index("document id 2", data2) + if err != nil { + panic(err) + } + + // 2 documents have been indexed + count, err := exampleIndex.DocCount() + if err != nil { + panic(err) + } + + fmt.Println(count) + // Output: + // 2 +} + +// Examples for query related functions + +func ExampleNewMatchQuery() { + // finds documents with fields fully matching the given query text + query := NewMatchQuery("named one") + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 1 +} + +func ExampleNewMatchAllQuery() { + // finds all documents in the index + query := NewMatchAllQuery() + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(len(searchResults.Hits)) + // Output: + // 2 +} + +func ExampleNewMatchNoneQuery() { + // matches no documents in the index + query := NewMatchNoneQuery() + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(len(searchResults.Hits)) + // Output: + // 0 +} + +func ExampleNewMatchPhraseQuery() { + // finds all documents with the given phrase in the index + query := NewMatchPhraseQuery("nameless one") + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 2 +} + +func ExampleNewNumericRangeQuery() { + value1 := float64(11) + value2 := float64(100) + data := struct{ Priority float64 }{Priority: float64(15)} + data2 := struct{ Priority float64 }{Priority: float64(10)} + + err = exampleIndex.Index("document id 3", data) + if err != nil { + panic(err) + } + err = exampleIndex.Index("document id 4", data2) + if err != nil { + panic(err) + } + + query := NewNumericRangeQuery(&value1, &value2) + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 3 +} + +func ExampleNewNumericRangeInclusiveQuery() { + value1 := float64(10) + value2 := float64(100) + v1incl := false + v2incl := false + + query := NewNumericRangeInclusiveQuery(&value1, &value2, &v1incl, &v2incl) + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 3 +} + +func ExampleNewPhraseQuery() { + // finds all documents with the given phrases in the given field in the index + query := NewPhraseQuery([]string{"nameless", "one"}, "Name") + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 2 +} + +func ExampleNewPrefixQuery() { + // finds all documents with terms having the given prefix in the index + query := NewPrefixQuery("name") + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(len(searchResults.Hits)) + // Output: + // 2 +} + +func ExampleNewQueryStringQuery() { + query := NewQueryStringQuery("+one -great") + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 1 +} + +func ExampleNewTermQuery() { + query := NewTermQuery("great") + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 2 +} + +func ExampleNewFacetRequest() { + facet := NewFacetRequest("Name", 1) + query := NewMatchAllQuery() + searchRequest := NewSearchRequest(query) + searchRequest.AddFacet("facet name", facet) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + // total number of terms + fmt.Println(searchResults.Facets["facet name"].Total) + // numer of docs with no value for this field + fmt.Println(searchResults.Facets["facet name"].Missing) + // term with highest occurrences in field name + fmt.Println(searchResults.Facets["facet name"].Terms.Terms()[0].Term) + // Output: + // 5 + // 2 + // one +} + +func ExampleFacetRequest_AddDateTimeRange() { + facet := NewFacetRequest("Created", 1) + facet.AddDateTimeRange("range name", time.Unix(0, 0), time.Now()) + query := NewMatchAllQuery() + searchRequest := NewSearchRequest(query) + searchRequest.AddFacet("facet name", facet) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + // dates in field Created since starting of unix time till now + fmt.Println(searchResults.Facets["facet name"].DateRanges[0].Count) + // Output: + // 2 +} + +func ExampleFacetRequest_AddNumericRange() { + value1 := float64(11) + + facet := NewFacetRequest("Priority", 1) + facet.AddNumericRange("range name", &value1, nil) + query := NewMatchAllQuery() + searchRequest := NewSearchRequest(query) + searchRequest.AddFacet("facet name", facet) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + // number documents with field Priority in the given range + fmt.Println(searchResults.Facets["facet name"].NumericRanges[0].Count) + // Output: + // 1 +} + +func ExampleNewHighlight() { + query := NewMatchQuery("nameless") + searchRequest := NewSearchRequest(query) + searchRequest.Highlight = NewHighlight() + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].Fragments["Name"][0]) + // Output: + // great nameless one +} + +func ExampleNewHighlightWithStyle() { + query := NewMatchQuery("nameless") + searchRequest := NewSearchRequest(query) + searchRequest.Highlight = NewHighlightWithStyle(ansi.Name) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].Fragments["Name"][0]) + // Output: + // great nameless one +} + +func ExampleSearchRequest_AddFacet() { + facet := NewFacetRequest("Name", 1) + query := NewMatchAllQuery() + searchRequest := NewSearchRequest(query) + searchRequest.AddFacet("facet name", facet) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + // total number of terms + fmt.Println(searchResults.Facets["facet name"].Total) + // numer of docs with no value for this field + fmt.Println(searchResults.Facets["facet name"].Missing) + // term with highest occurrences in field name + fmt.Println(searchResults.Facets["facet name"].Terms.Terms()[0].Term) + // Output: + // 5 + // 2 + // one +} + +func ExampleNewSearchRequest() { + // finds documents with fields fully matching the given query text + query := NewMatchQuery("named one") + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 1 +} + +func ExampleNewBooleanQuery() { + must := NewMatchQuery("one") + mustNot := NewMatchQuery("great") + query := NewBooleanQuery() + query.AddMust(must) + query.AddMustNot(mustNot) + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 1 +} + +func ExampleNewConjunctionQuery() { + conjunct1 := NewMatchQuery("great") + conjunct2 := NewMatchQuery("one") + query := NewConjunctionQuery(conjunct1, conjunct2) + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + // Output: + // document id 2 +} + +func ExampleNewDisjunctionQuery() { + disjunct1 := NewMatchQuery("great") + disjunct2 := NewMatchQuery("named") + query := NewDisjunctionQuery(disjunct1, disjunct2) + searchRequest := NewSearchRequest(query) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(len(searchResults.Hits)) + // Output: + // 2 +} + +func ExampleSearchRequest_SortBy() { + // find docs containing "one", order by Age instead of score + query := NewMatchQuery("one") + searchRequest := NewSearchRequest(query) + searchRequest.SortBy([]string{"Age"}) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + fmt.Println(searchResults.Hits[1].ID) + // Output: + // document id 2 + // document id 1 +} + +func ExampleSearchRequest_SortByCustom() { + // find all docs, order by Age, with docs missing Age field first + query := NewMatchAllQuery() + searchRequest := NewSearchRequest(query) + searchRequest.SortByCustom(search.SortOrder{ + &search.SortField{ + Field: "Age", + Missing: search.SortFieldMissingFirst, + }, + &search.SortDocID{}, + }) + searchResults, err := exampleIndex.Search(searchRequest) + if err != nil { + panic(err) + } + + fmt.Println(searchResults.Hits[0].ID) + fmt.Println(searchResults.Hits[1].ID) + fmt.Println(searchResults.Hits[2].ID) + fmt.Println(searchResults.Hits[3].ID) + // Output: + // document id 3 + // document id 4 + // document id 2 + // document id 1 +} diff --git a/geo/README.md b/geo/README.md new file mode 100644 index 0000000..9cb73df --- /dev/null +++ b/geo/README.md @@ -0,0 +1,283 @@ +# Geo spatial search support in bleve + +Latest bleve spatial capabilities are powered by spatial hierarchical tokens generated from s2geometry. +You can find more details about the [s2geometry basics here](http://s2geometry.io/), and explore the +extended functionality of our forked golang port of [s2geometry lib here](https://github.com/blevesearch/geo). + +Users can continue to index and query `geopoint` field type and the existing queries like, + +- Point Distance +- Bounded Rectangle +- Bounded Polygon + +as before. + +## New Spatial Field Type - geoshape + +We have introduced a field type (`geoshape`) for representing the new spatial types. + +Using the new `geoshape` field type, users can unblock the spatial capabilities +for the [geojson](https://datatracker.ietf.org/doc/html/rfc7946) shapes like, + +- Point +- LineString +- Polygon +- MultiPoint +- MultiLineString +- MultiPolygon +- GeometryCollection + +In addition to these shapes, bleve will also support additional shapes like, + +- Circle +- Envelope (Bounded box) + +To specify GeoJSON data, use a nested field with: + +- a field named type that specifies the GeoJSON object type and the type value will be case-insensitive. +- a field named coordinates that specifies the object's coordinates. + +``` + "fieldName": { + "type": "GeoJSON Type", + "coordinates": + } +``` + +- If specifying latitude and longitude coordinates, list the longitude first and then latitude. +- Valid longitude values are between -180 and 180, both inclusive. +- Valid latitude values are between -90 and 90, both inclusive. +- Shapes would be internally represented as geodesics. +- The GeoJSON specification strongly suggests splitting geometries so that neither of their parts crosses the antimeridian. + + +Examples for the various geojson shapes representations are as below. + +## Point + +The following specifies a [Point](https://tools.ietf.org/html/rfc7946#section-3.1.2) field in a document: + +``` + { + "type": "point", + "coordinates": [75.05687713623047,22.53539059204079] + } +``` + +## Linestring + +The following specifies a [Linestring](https://tools.ietf.org/html/rfc7946#section-3.1.4) field in a document: + + +``` +{ + "type": "linestring", + "coordinates": [ + [ 77.01416015625, 23.0797317624497], + [ 78.134765625, 20.385825381874263] + ] +} +``` + + +## Polygon + +The following specifies a [Polygon](https://tools.ietf.org/html/rfc7946#section-3.1.6) field in a document: + +``` +{ + "type": "polygon", + "coordinates": [ [ [ 85.605, 57.207], + [ 86.396, 55.998], + [ 87.033, 56.716], + [ 85.605, 57.207] + ] ] +} +``` + + +The first and last coordinates must match in order to close the polygon. +And the exterior coordinates have to be in Counter Clockwise Order in a polygon. (CCW) + + +## MultiPoint + +The following specifies a [Multipoint](https://tools.ietf.org/html/rfc7946#section-3.1.3) field in a document: + +``` +{ + "type": "multipoint", + "coordinates": [ + [ -115.8343505859375, 38.45789034424927], + [ -115.81237792968749, 38.19502155795575], + [ -120.80017089843749, 36.54053616262899], + [ -120.67932128906249, 36.33725319397006] + ] +} +``` + +## MultiLineString + +The following specifies a [MultiLineString](https://tools.ietf.org/html/rfc7946#section-3.1.5) field in a document: + +``` +{ + "type": "multilinestring", + "coordinates": [ + [ [ -118.31726074, 35.250105158],[ -117.509765624, 35.3756141] ], + [ [ -118.6962890, 34.624167789],[ -118.317260742, 35.03899204] ], + [ [ -117.9492187, 35.146862906], [ -117.6745605, 34.41144164] ] +] +} +``` + +## MultiPolygon + +The following specifies a [MultiPolygon](https://tools.ietf.org/html/rfc7946#section-3.1.7) field in a document: + +``` +{ + "type": "multipolygon", + "coordinates": [ + [ [ [ -73.958, 40.8003 ], [ -73.9498, 40.7968 ], + [ -73.9737, 40.7648 ], [ -73.9814, 40.7681 ], + [ -73.958, 40.8003 ] ] ], + + + [ [ [ -73.958, 40.8003 ], [ -73.9498, 40.7968 ], + [ -73.9737, 40.7648 ], [ -73.958, 40.8003 ] ] ] + ] +} +``` + + +## GeometryCollection + +The following specifies a [GeometryCollection](https://tools.ietf.org/html/rfc7946#section-3.1.8) field in a document: + +``` +{ + "type": "geometrycollection", + "geometries": [ + { + "type": "multipoint", + "coordinates": [ + [ -73.9580, 40.8003 ], + [ -73.9498, 40.7968 ], + [ -73.9737, 40.7648 ], + [ -73.9814, 40.7681 ] + ] + }, + { + "type": "multilinestring", + "coordinates": [ + [ [ -73.96943, 40.78519 ], [ -73.96082, 40.78095 ] ], + [ [ -73.96415, 40.79229 ], [ -73.95544, 40.78854 ] ], + [ [ -73.97162, 40.78205 ], [ -73.96374, 40.77715 ] ], + [ [ -73.97880, 40.77247 ], [ -73.97036, 40.76811 ] ] + ] + }, + { + "type" : "polygon", + "coordinates" : [ + [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ], + [ [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ] ] + ] + } +] +} +``` + + +## Circle + +If the user wishes to cover a circular region over the earth’s surface, then they could use this shape. +A sample circular shape is as below. + +``` +{ + "type": "circle", + "coordinates": [75.05687713623047,22.53539059204079], + "radius": "1000m" +} +``` + + +Circle is specified over the center point coordinates along with the radius. +Example formats supported for radius are: +"5in" , "5inch" , "7yd" , "7yards", "9ft" , "9feet", "11km", "11kilometers", "3nm" +"3nauticalmiles", "13mm" , "13millimeters", "15cm", "15centimeters", "17mi", "17miles" "19m" or "19meters". + +If the unit cannot be determined, the entire string is parsed and the unit of meters is assumed. + + +## Envelope + +Envelope type, which consists of coordinates for upper left and lower right points of the shape +to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]]. + +``` +{ + "type": "envelope", + "coordinates": [ + [72.83, 18.979], + [78.508,17.4555] + ] +} +``` + + +## GeoShape Query + +Geoshape query support three types/filters of spatial querying capability across those +heterogeneous types of documents indexed. + +### Query Structure: + +``` +{ + "query": { + "geometry": { + "shape": { + "type": "", + "coordinates": [[[ ]]] + }, + "relation": "<>" + } + } +} +``` + + +*shapeType* => can be any of the aforementioned types like Point, LineString, Polygon, MultiPoint, +Geometrycollection, MultiLineString, MultiPolygon, Circle and Envelope. + +*filterName* => can be any of the 3 types like *intersects*, *contains* and *within*. + +### Relation + +| FilterName | Description | +| :-----------:| :-----------------------------------------------------------------: | +| `intersects` | Return all documents whose shape field intersects the query geometry. | +| `contains` | Return all documents whose shape field contains the query geometry | +| `within` | Return all documents whose shape field is within the query geometry. | + +------------------------------------------------------------------------------------------------------------------------ + + + +### Older Implementation + +First, all of this geo code is a Go adaptation of the [Lucene 5.3.2 sandbox geo support](https://lucene.apache.org/core/5_3_2/sandbox/org/apache/lucene/util/package-summary.html). + +## Notes + +- All of the APIs will use float64 for lon/lat values. +- When describing a point in function arguments or return values, we always use the order lon, lat. +- High level APIs will use TopLeft and BottomRight to describe bounding boxes. This may not map cleanly to min/max lon/lat when crossing the dateline. The lower level APIs will use min/max lon/lat and require the higher-level code to split boxes accordingly. +- Points and MultiPoints may only contain Points and MultiPoints. +- LineStrings and MultiLineStrings may only contain Points and MultiPoints. +- Polygons or MultiPolygons intersecting Polygons and MultiPolygons may return arbitrary results when the overlap is only an edge or a vertex. +- Circles containing polygon will return a false positive result if all of the vertices of the polygon are within the circle, but the orientation of those points are clock-wise. +- The edges of an Envelope follows the latitude and logitude lines instead of the shortest path on a globe. +- Envelope intersecting queries with LineStrings, MultiLineStrings, Polygons and MultiPolygons implicitly converts the Envelope into a Polygon which changes the curvature of the edges causing inaccurate results for few edge cases. diff --git a/geo/benchmark_geohash_test.go b/geo/benchmark_geohash_test.go new file mode 100644 index 0000000..2cdeb1d --- /dev/null +++ b/geo/benchmark_geohash_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "testing" +) + +func BenchmarkGeoHashLen5NewDecode(b *testing.B) { + b.ResetTimer() + hash := "d3hn3" + for i := 0; i < b.N; i++ { + _, _ = DecodeGeoHash(hash) + } +} + +func BenchmarkGeoHashLen6NewDecode(b *testing.B) { + b.ResetTimer() + hash := "u4pruy" + for i := 0; i < b.N; i++ { + _, _ = DecodeGeoHash(hash) + } +} + +func BenchmarkGeoHashLen7NewDecode(b *testing.B) { + b.ResetTimer() + hash := "u4pruyd" + for i := 0; i < b.N; i++ { + _, _ = DecodeGeoHash(hash) + } +} diff --git a/geo/geo.go b/geo/geo.go new file mode 100644 index 0000000..2416c03 --- /dev/null +++ b/geo/geo.go @@ -0,0 +1,210 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "fmt" + "math" + + "github.com/blevesearch/bleve/v2/numeric" +) + +// GeoBits is the number of bits used for a single geo point +// Currently this is 32bits for lon and 32bits for lat +var GeoBits uint = 32 + +var minLon = -180.0 +var minLat = -90.0 +var maxLon = 180.0 +var maxLat = 90.0 +var minLonRad = minLon * degreesToRadian +var minLatRad = minLat * degreesToRadian +var maxLonRad = maxLon * degreesToRadian +var maxLatRad = maxLat * degreesToRadian +var geoTolerance = 1e-6 +var lonScale = float64((uint64(0x1)<> 1)) +} + +func unscaleLon(lon uint64) float64 { + return (float64(lon) / lonScale) + minLon +} + +func unscaleLat(lat uint64) float64 { + return (float64(lat) / latScale) + minLat +} + +// compareGeo will compare two float values and see if they are the same +// taking into consideration a known geo tolerance. +func compareGeo(a, b float64) float64 { + compare := a - b + if math.Abs(compare) <= geoTolerance { + return 0 + } + return compare +} + +// RectIntersects checks whether rectangles a and b intersect +func RectIntersects(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY float64) bool { + return !(aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) +} + +// RectWithin checks whether box a is within box b +func RectWithin(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY float64) bool { + rv := !(aMinX < bMinX || aMinY < bMinY || aMaxX > bMaxX || aMaxY > bMaxY) + return rv +} + +// BoundingBoxContains checks whether the lon/lat point is within the box +func BoundingBoxContains(lon, lat, minLon, minLat, maxLon, maxLat float64) bool { + return compareGeo(lon, minLon) >= 0 && compareGeo(lon, maxLon) <= 0 && + compareGeo(lat, minLat) >= 0 && compareGeo(lat, maxLat) <= 0 +} + +const degreesToRadian = math.Pi / 180 +const radiansToDegrees = 180 / math.Pi + +// DegreesToRadians converts an angle in degrees to radians +func DegreesToRadians(d float64) float64 { + return d * degreesToRadian +} + +// RadiansToDegrees converts an angle in radians to degress +func RadiansToDegrees(r float64) float64 { + return r * radiansToDegrees +} + +var earthMeanRadiusMeters = 6371008.7714 + +func RectFromPointDistance(lon, lat, dist float64) (float64, float64, float64, float64, error) { + err := checkLongitude(lon) + if err != nil { + return 0, 0, 0, 0, err + } + err = checkLatitude(lat) + if err != nil { + return 0, 0, 0, 0, err + } + radLon := DegreesToRadians(lon) + radLat := DegreesToRadians(lat) + radDistance := (dist + 7e-2) / earthMeanRadiusMeters + + minLatL := radLat - radDistance + maxLatL := radLat + radDistance + + var minLonL, maxLonL float64 + if minLatL > minLatRad && maxLatL < maxLatRad { + deltaLon := math.Asin(math.Sin(radDistance) / math.Cos(radLat)) + minLonL = radLon - deltaLon + if minLonL < minLonRad { + minLonL += 2 * math.Pi + } + maxLonL = radLon + deltaLon + if maxLonL > maxLonRad { + maxLonL -= 2 * math.Pi + } + } else { + // pole is inside distance + minLatL = math.Max(minLatL, minLatRad) + maxLatL = math.Min(maxLatL, maxLatRad) + minLonL = minLonRad + maxLonL = maxLonRad + } + + return RadiansToDegrees(minLonL), + RadiansToDegrees(maxLatL), + RadiansToDegrees(maxLonL), + RadiansToDegrees(minLatL), + nil +} + +func checkLatitude(latitude float64) error { + if math.IsNaN(latitude) || latitude < minLat || latitude > maxLat { + return fmt.Errorf("invalid latitude %f; must be between %f and %f", latitude, minLat, maxLat) + } + return nil +} + +func checkLongitude(longitude float64) error { + if math.IsNaN(longitude) || longitude < minLon || longitude > maxLon { + return fmt.Errorf("invalid longitude %f; must be between %f and %f", longitude, minLon, maxLon) + } + return nil +} + +func BoundingRectangleForPolygon(polygon []Point) ( + float64, float64, float64, float64, error) { + err := checkLongitude(polygon[0].Lon) + if err != nil { + return 0, 0, 0, 0, err + } + err = checkLatitude(polygon[0].Lat) + if err != nil { + return 0, 0, 0, 0, err + } + maxY, minY := polygon[0].Lat, polygon[0].Lat + maxX, minX := polygon[0].Lon, polygon[0].Lon + for i := 1; i < len(polygon); i++ { + err := checkLongitude(polygon[i].Lon) + if err != nil { + return 0, 0, 0, 0, err + } + err = checkLatitude(polygon[i].Lat) + if err != nil { + return 0, 0, 0, 0, err + } + + maxY = math.Max(maxY, polygon[i].Lat) + minY = math.Min(minY, polygon[i].Lat) + + maxX = math.Max(maxX, polygon[i].Lon) + minX = math.Min(minX, polygon[i].Lon) + } + + return minX, maxY, maxX, minY, nil +} diff --git a/geo/geo_dist.go b/geo/geo_dist.go new file mode 100644 index 0000000..3e6784f --- /dev/null +++ b/geo/geo_dist.go @@ -0,0 +1,98 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "fmt" + "math" + "strconv" + "strings" +) + +type distanceUnit struct { + conv float64 + suffixes []string +} + +var inch = distanceUnit{0.0254, []string{"in", "inch"}} +var yard = distanceUnit{0.9144, []string{"yd", "yards"}} +var feet = distanceUnit{0.3048, []string{"ft", "feet"}} +var kilom = distanceUnit{1000, []string{"km", "kilometers"}} +var nauticalm = distanceUnit{1852.0, []string{"nm", "nauticalmiles"}} +var millim = distanceUnit{0.001, []string{"mm", "millimeters"}} +var centim = distanceUnit{0.01, []string{"cm", "centimeters"}} +var miles = distanceUnit{1609.344, []string{"mi", "miles"}} +var meters = distanceUnit{1, []string{"m", "meters"}} + +var distanceUnits = []*distanceUnit{ + &inch, &yard, &feet, &kilom, &nauticalm, &millim, ¢im, &miles, &meters, +} + +// ParseDistance attempts to parse a distance string and return distance in +// meters. Example formats supported: +// "5in" "5inch" "7yd" "7yards" "9ft" "9feet" "11km" "11kilometers" +// "3nm" "3nauticalmiles" "13mm" "13millimeters" "15cm" "15centimeters" +// "17mi" "17miles" "19m" "19meters" +// If the unit cannot be determined, the entire string is parsed and the +// unit of meters is assumed. +// If the number portion cannot be parsed, 0 and the parse error are returned. +func ParseDistance(d string) (float64, error) { + for _, unit := range distanceUnits { + for _, unitSuffix := range unit.suffixes { + if strings.HasSuffix(d, unitSuffix) { + parsedNum, err := strconv.ParseFloat(d[0:len(d)-len(unitSuffix)], 64) + if err != nil { + return 0, err + } + return parsedNum * unit.conv, nil + } + } + } + // no unit matched, try assuming meters? + parsedNum, err := strconv.ParseFloat(d, 64) + if err != nil { + return 0, err + } + return parsedNum, nil +} + +// ParseDistanceUnit attempts to parse a distance unit and return the +// multiplier for converting this to meters. If the unit cannot be parsed +// then 0 and the error message is returned. +func ParseDistanceUnit(u string) (float64, error) { + for _, unit := range distanceUnits { + for _, unitSuffix := range unit.suffixes { + if u == unitSuffix { + return unit.conv, nil + } + } + } + return 0, fmt.Errorf("unknown distance unit: %s", u) +} + +// Haversin computes the distance between two points. +// This implemenation uses the sloppy math implemenations which trade off +// accuracy for performance. The distance returned is in kilometers. +func Haversin(lon1, lat1, lon2, lat2 float64) float64 { + x1 := lat1 * degreesToRadian + x2 := lat2 * degreesToRadian + h1 := 1 - math.Cos(x1-x2) + h2 := 1 - math.Cos((lon1-lon2)*degreesToRadian) + h := (h1 + math.Cos(x1)*math.Cos(x2)*h2) / 2 + avgLat := (x1 + x2) / 2 + diameter := earthDiameter(avgLat) + + return diameter * math.Asin(math.Min(1, math.Sqrt(h))) +} diff --git a/geo/geo_dist_test.go b/geo/geo_dist_test.go new file mode 100644 index 0000000..a7d67b2 --- /dev/null +++ b/geo/geo_dist_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "fmt" + "math" + "reflect" + "strconv" + "testing" +) + +func TestParseDistance(t *testing.T) { + tests := []struct { + dist string + want float64 + wantErr error + }{ + {"5mi", 5 * 1609.344, nil}, + {"3", 3, nil}, + {"3m", 3, nil}, + {"5km", 5000, nil}, + {"km", 0, &strconv.NumError{Func: "ParseFloat", Num: "", Err: strconv.ErrSyntax}}, + {"", 0, &strconv.NumError{Func: "ParseFloat", Num: "", Err: strconv.ErrSyntax}}, + } + + for _, test := range tests { + got, err := ParseDistance(test.dist) + if !reflect.DeepEqual(err, test.wantErr) { + t.Errorf("expected err: %v, got %v for %s", test.wantErr, err, test.dist) + } + if got != test.want { + t.Errorf("expected distance %f got %f for %s", test.want, got, test.dist) + } + } +} + +func TestParseDistanceUnit(t *testing.T) { + tests := []struct { + dist string + want float64 + wantErr error + }{ + {"mi", 1609.344, nil}, + {"m", 1, nil}, + {"km", 1000, nil}, + {"", 0, fmt.Errorf("unknown distance unit: ")}, + {"kam", 0, fmt.Errorf("unknown distance unit: kam")}, + } + + for _, test := range tests { + got, err := ParseDistanceUnit(test.dist) + if !reflect.DeepEqual(err, test.wantErr) { + t.Errorf("expected err: %v, got %v for %s", test.wantErr, err, test.dist) + } + if got != test.want { + t.Errorf("expected distance %f got %f for %s", test.want, got, test.dist) + } + } +} + +func TestHaversinDistance(t *testing.T) { + earthRadiusKMs := 6378.137 + halfCircle := earthRadiusKMs * math.Pi + + tests := []struct { + lon1 float64 + lat1 float64 + lon2 float64 + lat2 float64 + want float64 + }{ + {1, 1, math.NaN(), 1, math.NaN()}, + {1, 1, 1, math.NaN(), math.NaN()}, + {1, math.NaN(), 1, 1, math.NaN()}, + {math.NaN(), 1, 1, 1, math.NaN()}, + + {0, 0, 0, 0, 0}, + {-180, 0, -180, 0, 0}, + {-180, 0, 180, 0, 0}, + {180, 0, 180, 0, 0}, + + {0, 90, 0, 90, 0}, + {-180, 90, -180, 90, 0}, + {-180, 90, 180, 90, 0}, + {180, 90, 180, 90, 0}, + + {0, 0, 180, 0, halfCircle}, + + {-74.0059731, 40.7143528, -74.0059731, 40.7143528, 0}, + {-74.0059731, 40.7143528, -73.9844722, 40.759011, 5.286}, + {-74.0059731, 40.7143528, -74.007819, 40.718266, 0.4621}, + {-74.0059731, 40.7143528, -74.0088305, 40.7051157, 1.055}, + {-74.0059731, 40.7143528, -74, 40.7247222, 1.258}, + {-74.0059731, 40.7143528, -73.9962255, 40.731033, 2.029}, + {-74.0059731, 40.7143528, -73.95, 40.65, 8.572}, + } + + for _, test := range tests { + got := Haversin(test.lon1, test.lat1, test.lon2, test.lat2) + if math.IsNaN(test.want) && !math.IsNaN(got) { + t.Errorf("expected NaN, got %f", got) + } + if !math.IsNaN(test.want) && math.Abs(got-test.want) > 1e-2 { + t.Errorf("expected %f got %f", test.want, got) + } + } +} diff --git a/geo/geo_s2plugin_impl.go b/geo/geo_s2plugin_impl.go new file mode 100644 index 0000000..6acac5a --- /dev/null +++ b/geo/geo_s2plugin_impl.go @@ -0,0 +1,463 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "encoding/json" + "sync" + + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" + "github.com/blevesearch/geo/geojson" + "github.com/blevesearch/geo/s2" +) + +const ( + PointType = "point" + MultiPointType = "multipoint" + LineStringType = "linestring" + MultiLineStringType = "multilinestring" + PolygonType = "polygon" + MultiPolygonType = "multipolygon" + GeometryCollectionType = "geometrycollection" + CircleType = "circle" + EnvelopeType = "envelope" +) + +// spatialPluginsMap is spatial plugin cache. +var ( + spatialPluginsMap = make(map[string]index.SpatialAnalyzerPlugin) + pluginsMapLock = sync.RWMutex{} +) + +func init() { + registerS2RegionTermIndexer() +} + +func registerS2RegionTermIndexer() { + spatialPlugin := S2SpatialAnalyzerPlugin{ + s2Indexer: s2.NewRegionTermIndexerWithOptions(initS2IndexerOptions()), + s2Searcher: s2.NewRegionTermIndexerWithOptions(initS2SearcherOptions()), + s2GeoPointsRegionTermIndexer: s2.NewRegionTermIndexerWithOptions(initS2OptionsForGeoPoints()), + } + + RegisterSpatialAnalyzerPlugin(&spatialPlugin) +} + +// RegisterSpatialAnalyzerPlugin registers the given plugin implementation. +func RegisterSpatialAnalyzerPlugin(plugin index.SpatialAnalyzerPlugin) { + pluginsMapLock.Lock() + spatialPluginsMap[plugin.Type()] = plugin + pluginsMapLock.Unlock() +} + +// GetSpatialAnalyzerPlugin retrieves the given implementation type. +func GetSpatialAnalyzerPlugin(typ string) index.SpatialAnalyzerPlugin { + pluginsMapLock.RLock() + rv := spatialPluginsMap[typ] + pluginsMapLock.RUnlock() + return rv +} + +// initS2IndexerOptions returns the options for s2's region +// term indexer for the index time tokens of geojson shapes. +func initS2IndexerOptions() s2.Options { + options := s2.Options{} + // maxLevel control the maximum size of the + // S2Cells used to approximate regions. + options.SetMaxLevel(16) + + // minLevel control the minimum size of the + // S2Cells used to approximate regions. + options.SetMinLevel(2) + + // levelMod value greater than 1 increases the effective branching + // factor of the S2Cell hierarchy by skipping some levels. + options.SetLevelMod(1) + + // maxCells controls the maximum number of cells + // when approximating each s2 region. + options.SetMaxCells(20) + + return options +} + +// initS2SearcherOptions returns the options for s2's region +// term indexer for the query time tokens of geojson shapes. +func initS2SearcherOptions() s2.Options { + options := s2.Options{} + // maxLevel control the maximum size of the + // S2Cells used to approximate regions. + options.SetMaxLevel(16) + + // minLevel control the minimum size of the + // S2Cells used to approximate regions. + options.SetMinLevel(2) + + // levelMod value greater than 1 increases the effective branching + // factor of the S2Cell hierarchy by skipping some levels. + options.SetLevelMod(1) + + // maxCells controls the maximum number of cells + // when approximating each s2 region. + options.SetMaxCells(8) + + return options +} + +// initS2OptionsForGeoPoints returns the options for +// s2's region term indexer for the original geopoints. +func initS2OptionsForGeoPoints() s2.Options { + options := s2.Options{} + // maxLevel control the maximum size of the + // S2Cells used to approximate regions. + options.SetMaxLevel(16) + + // minLevel control the minimum size of the + // S2Cells used to approximate regions. + options.SetMinLevel(4) + + // levelMod value greater than 1 increases the effective branching + // factor of the S2Cell hierarchy by skipping some levels. + options.SetLevelMod(2) + + // maxCells controls the maximum number of cells + // when approximating each s2 region. + options.SetMaxCells(8) + + // explicit for geo points. + options.SetPointsOnly(true) + + return options +} + +// S2SpatialAnalyzerPlugin is an implementation of +// the index.SpatialAnalyzerPlugin interface. +type S2SpatialAnalyzerPlugin struct { + s2Indexer *s2.RegionTermIndexer + s2Searcher *s2.RegionTermIndexer + s2GeoPointsRegionTermIndexer *s2.RegionTermIndexer +} + +func (s *S2SpatialAnalyzerPlugin) Type() string { + return "s2" +} + +func (s *S2SpatialAnalyzerPlugin) GetIndexTokens(queryShape index.GeoJSON) []string { + var rv []string + shapes := []index.GeoJSON{queryShape} + if gc, ok := queryShape.(*geojson.GeometryCollection); ok { + shapes = gc.Shapes + } + + for _, shape := range shapes { + if s2t, ok := shape.(s2Tokenizable); ok { + rv = append(rv, s2t.IndexTokens(s.s2Indexer)...) + } else if s2t, ok := shape.(s2TokenizableEx); ok { + rv = append(rv, s2t.IndexTokens(s)...) + } + } + + return geojson.DeduplicateTerms(rv) +} + +func (s *S2SpatialAnalyzerPlugin) GetQueryTokens(queryShape index.GeoJSON) []string { + var rv []string + shapes := []index.GeoJSON{queryShape} + if gc, ok := queryShape.(*geojson.GeometryCollection); ok { + shapes = gc.Shapes + } + + for _, shape := range shapes { + if s2t, ok := shape.(s2Tokenizable); ok { + rv = append(rv, s2t.QueryTokens(s.s2Searcher)...) + } else if s2t, ok := shape.(s2TokenizableEx); ok { + rv = append(rv, s2t.QueryTokens(s)...) + } + } + + return geojson.DeduplicateTerms(rv) +} + +// ------------------------------------------------------------------------ +// s2Tokenizable is an optional interface for shapes that support +// the generation of s2 based tokens that can be used for both +// indexing and querying. + +type s2Tokenizable interface { + // IndexTokens returns the tokens for indexing. + IndexTokens(*s2.RegionTermIndexer) []string + + // QueryTokens returns the tokens for searching. + QueryTokens(*s2.RegionTermIndexer) []string +} + +// ------------------------------------------------------------------------ +// s2TokenizableEx is an optional interface for shapes that support +// the generation of s2 based tokens that can be used for both +// indexing and querying. This is intended for the older geopoint +// indexing and querying. +type s2TokenizableEx interface { + // IndexTokens returns the tokens for indexing. + IndexTokens(*S2SpatialAnalyzerPlugin) []string + + // QueryTokens returns the tokens for searching. + QueryTokens(*S2SpatialAnalyzerPlugin) []string +} + +//---------------------------------------------------------------------------------- + +func (p *Point) Type() string { + return PointType +} + +func (p *Point) Value() ([]byte, error) { + return util.MarshalJSON(p) +} + +func (p *Point) Intersects(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (p *Point) Contains(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (p *Point) IndexTokens(s *S2SpatialAnalyzerPlugin) []string { + return s.s2GeoPointsRegionTermIndexer.GetIndexTermsForPoint(s2.PointFromLatLng( + s2.LatLngFromDegrees(p.Lat, p.Lon)), "") +} + +func (p *Point) QueryTokens(s *S2SpatialAnalyzerPlugin) []string { + return nil +} + +//---------------------------------------------------------------------------------- + +type boundedRectangle struct { + minLat float64 + maxLat float64 + minLon float64 + maxLon float64 +} + +func NewBoundedRectangle(minLat, minLon, maxLat, + maxLon float64) *boundedRectangle { + return &boundedRectangle{minLat: minLat, + maxLat: maxLat, minLon: minLon, maxLon: maxLon} +} + +func (br *boundedRectangle) Type() string { + // placeholder implementation + return "boundedRectangle" +} + +func (br *boundedRectangle) Value() ([]byte, error) { + return util.MarshalJSON(br) +} + +func (p *boundedRectangle) Intersects(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (p *boundedRectangle) Contains(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (br *boundedRectangle) IndexTokens(s *S2SpatialAnalyzerPlugin) []string { + return nil +} + +func (br *boundedRectangle) QueryTokens(s *S2SpatialAnalyzerPlugin) []string { + rect := s2.RectFromDegrees(br.minLat, br.minLon, br.maxLat, br.maxLon) + + // obtain the terms to be searched for the given bounding box. + terms := s.s2GeoPointsRegionTermIndexer.GetQueryTermsForRegion(rect, "") + + return geojson.StripCoveringTerms(terms) +} + +//---------------------------------------------------------------------------------- + +type boundedPolygon struct { + coordinates []Point +} + +func NewBoundedPolygon(coordinates []Point) *boundedPolygon { + return &boundedPolygon{coordinates: coordinates} +} + +func (bp *boundedPolygon) Type() string { + // placeholder implementation + return "boundedPolygon" +} + +func (bp *boundedPolygon) Value() ([]byte, error) { + return util.MarshalJSON(bp) +} + +func (p *boundedPolygon) Intersects(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (p *boundedPolygon) Contains(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (bp *boundedPolygon) IndexTokens(s *S2SpatialAnalyzerPlugin) []string { + return nil +} + +func (bp *boundedPolygon) QueryTokens(s *S2SpatialAnalyzerPlugin) []string { + vertices := make([]s2.Point, len(bp.coordinates)) + for i, point := range bp.coordinates { + vertices[i] = s2.PointFromLatLng( + s2.LatLngFromDegrees(point.Lat, point.Lon)) + } + s2polygon := s2.PolygonFromOrientedLoops([]*s2.Loop{s2.LoopFromPoints(vertices)}) + + // obtain the terms to be searched for the given polygon. + terms := s.s2GeoPointsRegionTermIndexer.GetQueryTermsForRegion( + s2polygon.CapBound(), "") + + return geojson.StripCoveringTerms(terms) +} + +//---------------------------------------------------------------------------------- + +type pointDistance struct { + dist float64 + centerLat float64 + centerLon float64 +} + +func (p *pointDistance) Type() string { + // placeholder implementation + return "pointDistance" +} + +func (p *pointDistance) Value() ([]byte, error) { + return util.MarshalJSON(p) +} + +func NewPointDistance(centerLat, centerLon, + dist float64) *pointDistance { + return &pointDistance{centerLat: centerLat, + centerLon: centerLon, dist: dist} +} + +func (p *pointDistance) Intersects(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (p *pointDistance) Contains(s index.GeoJSON) (bool, error) { + // placeholder implementation + return false, nil +} + +func (pd *pointDistance) IndexTokens(s *S2SpatialAnalyzerPlugin) []string { + return nil +} + +func (pd *pointDistance) QueryTokens(s *S2SpatialAnalyzerPlugin) []string { + // obtain the covering query region from the given points. + queryRegion := s2.CapFromCenterAndRadius(pd.centerLat, + pd.centerLon, pd.dist) + + // obtain the query terms for the query region. + terms := s.s2GeoPointsRegionTermIndexer.GetQueryTermsForRegion(queryRegion, "") + + return geojson.StripCoveringTerms(terms) +} + +// ------------------------------------------------------------------------ + +// NewGeometryCollection instantiate a geometrycollection +// and prefix the byte contents with certain glue bytes that +// can be used later while filering the doc values. +func NewGeometryCollection(coordinates [][][][][]float64, + typs []string) (index.GeoJSON, []byte, error) { + shapes := make([]*geojson.GeoShape, len(coordinates)) + for i := range coordinates { + shapes[i] = &geojson.GeoShape{ + Coordinates: coordinates[i], + Type: typs[i], + } + } + + return geojson.NewGeometryCollection(shapes) +} + +func NewGeometryCollectionFromShapes(shapes []*geojson.GeoShape) ( + index.GeoJSON, []byte, error) { + + return geojson.NewGeometryCollection(shapes) +} + +// NewGeoCircleShape instantiate a circle shape and +// prefix the byte contents with certain glue bytes that +// can be used later while filering the doc values. +func NewGeoCircleShape(cp []float64, + radius string) (index.GeoJSON, []byte, error) { + return geojson.NewGeoCircleShape(cp, radius) +} + +func NewGeoJsonShape(coordinates [][][][]float64, typ string) ( + index.GeoJSON, []byte, error) { + return geojson.NewGeoJsonShape(coordinates, typ) +} + +func NewGeoJsonPoint(points []float64) index.GeoJSON { + return geojson.NewGeoJsonPoint(points) +} + +func NewGeoJsonMultiPoint(points [][]float64) index.GeoJSON { + return geojson.NewGeoJsonMultiPoint(points) +} + +func NewGeoJsonLinestring(points [][]float64) index.GeoJSON { + return geojson.NewGeoJsonLinestring(points) +} + +func NewGeoJsonMultilinestring(points [][][]float64) index.GeoJSON { + return geojson.NewGeoJsonMultilinestring(points) +} + +func NewGeoJsonPolygon(points [][][]float64) index.GeoJSON { + return geojson.NewGeoJsonPolygon(points) +} + +func NewGeoJsonMultiPolygon(points [][][][]float64) index.GeoJSON { + return geojson.NewGeoJsonMultiPolygon(points) +} + +func NewGeoCircle(points []float64, radius string) index.GeoJSON { + return geojson.NewGeoCircle(points, radius) +} + +func NewGeoEnvelope(points [][]float64) index.GeoJSON { + return geojson.NewGeoEnvelope(points) +} + +func ParseGeoJSONShape(input json.RawMessage) (index.GeoJSON, error) { + return geojson.ParseGeoJSONShape(input) +} diff --git a/geo/geo_test.go b/geo/geo_test.go new file mode 100644 index 0000000..52a38e2 --- /dev/null +++ b/geo/geo_test.go @@ -0,0 +1,183 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "math" + "testing" +) + +func TestMortonHashMortonUnhash(t *testing.T) { + tests := []struct { + lon float64 + lat float64 + }{ + {-180.0, -90.0}, + {-5, 27.3}, + {0, 0}, + {1.0, 1.0}, + {24.7, -80.4}, + {180.0, 90.0}, + } + + for _, test := range tests { + hash := MortonHash(test.lon, test.lat) + lon := MortonUnhashLon(hash) + lat := MortonUnhashLat(hash) + if compareGeo(test.lon, lon) != 0 { + t.Errorf("expected lon %f, got %f, hash %x", test.lon, lon, hash) + } + if compareGeo(test.lat, lat) != 0 { + t.Errorf("expected lat %f, got %f, hash %x", test.lat, lat, hash) + } + } +} + +func TestScaleLonUnscaleLon(t *testing.T) { + tests := []struct { + lon float64 + }{ + {-180.0}, + {0.0}, + {1.0}, + {180.0}, + } + + for _, test := range tests { + s := scaleLon(test.lon) + lon := unscaleLon(s) + if compareGeo(test.lon, lon) != 0 { + t.Errorf("expected %f, got %f, scaled was %d", test.lon, lon, s) + } + } +} + +func TestScaleLatUnscaleLat(t *testing.T) { + tests := []struct { + lat float64 + }{ + {-90.0}, + {0.0}, + {1.0}, + {90.0}, + } + + for _, test := range tests { + s := scaleLat(test.lat) + lat := unscaleLat(s) + if compareGeo(test.lat, lat) != 0 { + t.Errorf("expected %.16f, got %.16f, scaled was %d", test.lat, lat, s) + } + } +} + +func TestRectFromPointDistance(t *testing.T) { + // at the equator 1 degree of latitude is about 110567 meters + _, upperLeftLat, _, lowerRightLat, err := RectFromPointDistance(0, 0, 110567) + if err != nil { + t.Fatal(err) + } + if math.Abs(upperLeftLat-1) > 1e-2 { + t.Errorf("expected bounding box upper left lat to be almost 1, got %f", upperLeftLat) + } + if math.Abs(lowerRightLat+1) > 1e-2 { + t.Errorf("expected bounding box lower right lat to be almost -1, got %f", lowerRightLat) + } +} + +func TestRectIntersects(t *testing.T) { + tests := []struct { + aMinX float64 + aMinY float64 + aMaxX float64 + aMaxY float64 + bMinX float64 + bMinY float64 + bMaxX float64 + bMaxY float64 + want bool + }{ + // clearly overlap + {0, 0, 2, 2, 1, 1, 3, 3, true}, + // clearly do not overalp + {0, 0, 1, 1, 2, 2, 3, 3, false}, + // share common point + {0, 0, 1, 1, 1, 1, 2, 2, true}, + } + + for _, test := range tests { + got := RectIntersects(test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + if test.want != got { + t.Errorf("expected intersects %t, got %t for %f %f %f %f %f %f %f %f", test.want, got, test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + } + } +} + +func TestRectWithin(t *testing.T) { + tests := []struct { + aMinX float64 + aMinY float64 + aMaxX float64 + aMaxY float64 + bMinX float64 + bMinY float64 + bMaxX float64 + bMaxY float64 + want bool + }{ + // clearly within + {1, 1, 2, 2, 0, 0, 3, 3, true}, + // clearly not within + {0, 0, 1, 1, 2, 2, 3, 3, false}, + // overlapping + {0, 0, 2, 2, 1, 1, 3, 3, false}, + // share common point + {0, 0, 1, 1, 1, 1, 2, 2, false}, + // within, but boxes reversed (b is within a, but not a within b) + {0, 0, 3, 3, 1, 1, 2, 2, false}, + } + + for _, test := range tests { + got := RectWithin(test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + if test.want != got { + t.Errorf("expected within %t, got %t for %f %f %f %f %f %f %f %f", test.want, got, test.aMinX, test.aMinY, test.aMaxX, test.aMaxY, test.bMinX, test.bMinY, test.bMaxX, test.bMaxY) + } + } +} + +func TestBoundingBoxContains(t *testing.T) { + tests := []struct { + lon float64 + lat float64 + minX float64 + minY float64 + maxX float64 + maxY float64 + want bool + }{ + // clearly contains + {1, 1, 0, 0, 2, 2, true}, + // clearly does not contain + {0, 0, 1, 1, 2, 2, false}, + // on corner + {0, 0, 0, 0, 2, 2, true}, + } + for _, test := range tests { + got := BoundingBoxContains(test.lon, test.lat, test.minX, test.minY, test.maxX, test.maxY) + if test.want != got { + t.Errorf("expected box contains %t, got %t for %f,%f in %f %f %f %f ", test.want, got, test.lon, test.lat, test.minX, test.minY, test.maxX, test.maxY) + } + } +} diff --git a/geo/geohash.go b/geo/geohash.go new file mode 100644 index 0000000..d3d4dfa --- /dev/null +++ b/geo/geohash.go @@ -0,0 +1,111 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// This implementation is inspired from the geohash-js +// ref: https://github.com/davetroy/geohash-js + +package geo + +// encoding encapsulates an encoding defined by a given base32 alphabet. +type encoding struct { + enc string + dec [256]byte +} + +// newEncoding constructs a new encoding defined by the given alphabet, +// which must be a 32-byte string. +func newEncoding(encoder string) *encoding { + e := new(encoding) + e.enc = encoder + for i := 0; i < len(e.dec); i++ { + e.dec[i] = 0xff + } + for i := 0; i < len(encoder); i++ { + e.dec[encoder[i]] = byte(i) + } + return e +} + +// base32encoding with the Geohash alphabet. +var base32encoding = newEncoding("0123456789bcdefghjkmnpqrstuvwxyz") + +var masks = []uint64{16, 8, 4, 2, 1} + +// DecodeGeoHash decodes the string geohash faster with +// higher precision. This api is in experimental phase. +func DecodeGeoHash(geoHash string) (float64, float64) { + even := true + lat := []float64{-90.0, 90.0} + lon := []float64{-180.0, 180.0} + + for i := 0; i < len(geoHash); i++ { + cd := uint64(base32encoding.dec[geoHash[i]]) + for j := 0; j < 5; j++ { + if even { + if cd&masks[j] > 0 { + lon[0] = (lon[0] + lon[1]) / 2 + } else { + lon[1] = (lon[0] + lon[1]) / 2 + } + } else { + if cd&masks[j] > 0 { + lat[0] = (lat[0] + lat[1]) / 2 + } else { + lat[1] = (lat[0] + lat[1]) / 2 + } + } + even = !even + } + } + + return (lat[0] + lat[1]) / 2, (lon[0] + lon[1]) / 2 +} + +func EncodeGeoHash(lat, lon float64) string { + even := true + lats := []float64{-90.0, 90.0} + lons := []float64{-180.0, 180.0} + precision := 12 + var ch, bit uint64 + var geoHash string + + for len(geoHash) < precision { + if even { + mid := (lons[0] + lons[1]) / 2 + if lon > mid { + ch |= masks[bit] + lons[0] = mid + } else { + lons[1] = mid + } + } else { + mid := (lats[0] + lats[1]) / 2 + if lat > mid { + ch |= masks[bit] + lats[0] = mid + } else { + lats[1] = mid + } + } + even = !even + if bit < 4 { + bit++ + } else { + geoHash += string(base32encoding.enc[ch]) + ch = 0 + bit = 0 + } + } + + return geoHash +} diff --git a/geo/geohash_test.go b/geo/geohash_test.go new file mode 100644 index 0000000..11b920d --- /dev/null +++ b/geo/geohash_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "strings" + "testing" +) + +func TestDecodeGeoHash(t *testing.T) { + tests := []struct { + hash string + lon float64 + lat float64 + }{ + {"d3hn3", -73.059082, 6.745605}, // -73.05908203, 6.74560547 as per http://geohash.co/ + {"u4pru", 10.393066, 57.634277}, // 10.39306641, 57.63427734 + {"u4pruy", 10.409546, 57.648010}, // 10.40954590, 57.64801025 + {"u4pruyd", 10.407486, 57.648697}, // 10.40748596, 57.64869690 + {"u4pruydqqvj", 10.40744, 57.64911}, // 10.40743969, 57.64911063 + } + + for _, test := range tests { + lat, lon := DecodeGeoHash(test.hash) + + if compareGeo(test.lon, lon) != 0 { + t.Errorf("expected lon %f, got %f, hash %s", test.lon, lon, test.hash) + } + if compareGeo(test.lat, lat) != 0 { + t.Errorf("expected lat %f, got %f, hash %s", test.lat, lat, test.hash) + } + } +} + +func TestEncodeGeoHash(t *testing.T) { + tests := []struct { + lon float64 + lat float64 + hash string + }{ + {2.29449034, 48.85841131, "u09tunquc"}, + {76.491540, 10.060349, "t9y3hx7my0fp"}, + } + + for _, test := range tests { + hash := EncodeGeoHash(test.lat, test.lon) + + if !strings.HasPrefix(hash, test.hash) { + t.Errorf("expected hash %s, got %s", test.hash, hash) + } + } +} diff --git a/geo/parse.go b/geo/parse.go new file mode 100644 index 0000000..7be0a06 --- /dev/null +++ b/geo/parse.go @@ -0,0 +1,465 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "reflect" + "strconv" + "strings" + + "github.com/blevesearch/bleve/v2/util" + "github.com/blevesearch/geo/geojson" +) + +// ExtractGeoPoint takes an arbitrary interface{} and tries it's best to +// interpret it is as geo point. Supported formats: +// Container: +// slice length 2 (GeoJSON) +// +// first element lon, second element lat +// +// string (coordinates separated by comma, or a geohash) +// +// first element lat, second element lon +// +// map[string]interface{} +// +// exact keys lat and lon or lng +// +// struct +// +// w/exported fields case-insensitive match on lat and lon or lng +// +// struct +// +// satisfying Later and Loner or Lnger interfaces +// +// in all cases values must be some sort of numeric-like thing: int/uint/float +func ExtractGeoPoint(thing interface{}) (lon, lat float64, success bool) { + var foundLon, foundLat bool + + thingVal := reflect.ValueOf(thing) + if !thingVal.IsValid() { + return lon, lat, false + } + + thingTyp := thingVal.Type() + + // is it a slice + if thingVal.Kind() == reflect.Slice { + // must be length 2 + if thingVal.Len() == 2 { + first := thingVal.Index(0) + if first.CanInterface() { + firstVal := first.Interface() + lon, foundLon = util.ExtractNumericValFloat64(firstVal) + } + second := thingVal.Index(1) + if second.CanInterface() { + secondVal := second.Interface() + lat, foundLat = util.ExtractNumericValFloat64(secondVal) + } + } + } + + // is it a string + if thingVal.Kind() == reflect.String { + geoStr := thingVal.Interface().(string) + if strings.Contains(geoStr, ",") { + // geo point with coordinates split by comma + points := strings.Split(geoStr, ",") + for i, point := range points { + // trim any leading or trailing white spaces + points[i] = strings.TrimSpace(point) + } + if len(points) == 2 { + var err error + lat, err = strconv.ParseFloat(points[0], 64) + if err == nil { + foundLat = true + } + lon, err = strconv.ParseFloat(points[1], 64) + if err == nil { + foundLon = true + } + } + } else { + // geohash + if len(geoStr) <= geoHashMaxLength { + lat, lon = DecodeGeoHash(geoStr) + foundLat = true + foundLon = true + } + } + } + + // is it a map + if l, ok := thing.(map[string]interface{}); ok { + if lval, ok := l["lon"]; ok { + lon, foundLon = util.ExtractNumericValFloat64(lval) + } else if lval, ok := l["lng"]; ok { + lon, foundLon = util.ExtractNumericValFloat64(lval) + } + if lval, ok := l["lat"]; ok { + lat, foundLat = util.ExtractNumericValFloat64(lval) + } + } + + // now try reflection on struct fields + if thingVal.Kind() == reflect.Struct { + for i := 0; i < thingVal.NumField(); i++ { + fieldName := thingTyp.Field(i).Name + if strings.HasPrefix(strings.ToLower(fieldName), "lon") { + if thingVal.Field(i).CanInterface() { + fieldVal := thingVal.Field(i).Interface() + lon, foundLon = util.ExtractNumericValFloat64(fieldVal) + } + } + if strings.HasPrefix(strings.ToLower(fieldName), "lng") { + if thingVal.Field(i).CanInterface() { + fieldVal := thingVal.Field(i).Interface() + lon, foundLon = util.ExtractNumericValFloat64(fieldVal) + } + } + if strings.HasPrefix(strings.ToLower(fieldName), "lat") { + if thingVal.Field(i).CanInterface() { + fieldVal := thingVal.Field(i).Interface() + lat, foundLat = util.ExtractNumericValFloat64(fieldVal) + } + } + } + } + + // last hope, some interfaces + // lon + if l, ok := thing.(loner); ok { + lon = l.Lon() + foundLon = true + } else if l, ok := thing.(lnger); ok { + lon = l.Lng() + foundLon = true + } + // lat + if l, ok := thing.(later); ok { + lat = l.Lat() + foundLat = true + } + + return lon, lat, foundLon && foundLat +} + +// various support interfaces which can be used to find lat/lon +type loner interface { + Lon() float64 +} + +type later interface { + Lat() float64 +} + +type lnger interface { + Lng() float64 +} + +// GlueBytes primarily for quicker filtering of docvalues +// during the filtering phase. +var GlueBytes = []byte("##") + +var GlueBytesOffset = len(GlueBytes) + +func extractCoordinates(thing interface{}) []float64 { + thingVal := reflect.ValueOf(thing) + if !thingVal.IsValid() { + return nil + } + + if thingVal.Kind() == reflect.Slice { + // must be length 2 + if thingVal.Len() == 2 { + var foundLon, foundLat bool + var lon, lat float64 + first := thingVal.Index(0) + if first.CanInterface() { + firstVal := first.Interface() + lon, foundLon = util.ExtractNumericValFloat64(firstVal) + } + second := thingVal.Index(1) + if second.CanInterface() { + secondVal := second.Interface() + lat, foundLat = util.ExtractNumericValFloat64(secondVal) + } + + if !foundLon || !foundLat { + return nil + } + + return []float64{lon, lat} + } + } + return nil +} + +func extract2DCoordinates(thing interface{}) [][]float64 { + thingVal := reflect.ValueOf(thing) + if !thingVal.IsValid() { + return nil + } + + rv := make([][]float64, 0, 8) + if thingVal.Kind() == reflect.Slice { + for j := 0; j < thingVal.Len(); j++ { + edges := thingVal.Index(j).Interface() + if es, ok := edges.([]interface{}); ok { + v := extractCoordinates(es) + if len(v) == 2 { + rv = append(rv, v) + } + } + } + + return rv + } + + return nil +} + +func extract3DCoordinates(thing interface{}) (c [][][]float64) { + coords := reflect.ValueOf(thing) + if !coords.IsValid() { + return nil + } + + if coords.Kind() == reflect.Slice { + for i := 0; i < coords.Len(); i++ { + vals := coords.Index(i) + edges := vals.Interface() + if es, ok := edges.([]interface{}); ok { + loop := extract2DCoordinates(es) + if len(loop) > 0 { + c = append(c, loop) + } + } + } + } + + return c +} + +func extract4DCoordinates(thing interface{}) (rv [][][][]float64) { + thingVal := reflect.ValueOf(thing) + if !thingVal.IsValid() { + return nil + } + + if thingVal.Kind() == reflect.Slice { + for j := 0; j < thingVal.Len(); j++ { + c := extract3DCoordinates(thingVal.Index(j).Interface()) + rv = append(rv, c) + } + } + + return rv +} + +func ParseGeoShapeField(thing interface{}) (interface{}, string, error) { + thingVal := reflect.ValueOf(thing) + if !thingVal.IsValid() { + return nil, "", nil + } + + var shape string + var coordValue interface{} + + if thingVal.Kind() == reflect.Map { + iter := thingVal.MapRange() + for iter.Next() { + if iter.Key().String() == "type" { + shape = iter.Value().Interface().(string) + continue + } + + if iter.Key().String() == "coordinates" { + coordValue = iter.Value().Interface() + } + } + } + + return coordValue, strings.ToLower(shape), nil +} + +func extractGeoShape(thing interface{}) (*geojson.GeoShape, bool) { + + coordValue, typ, err := ParseGeoShapeField(thing) + if err != nil { + return nil, false + } + + if typ == CircleType { + return ExtractCircle(thing) + } + + return ExtractGeoShapeCoordinates(coordValue, typ) +} + +// ExtractGeometryCollection takes an interface{} and tries it's best to +// interpret all the member geojson shapes within it. +func ExtractGeometryCollection(thing interface{}) ([]*geojson.GeoShape, bool) { + thingVal := reflect.ValueOf(thing) + if !thingVal.IsValid() { + return nil, false + } + var rv []*geojson.GeoShape + var f bool + + if thingVal.Kind() == reflect.Map { + iter := thingVal.MapRange() + for iter.Next() { + + if iter.Key().String() == "type" { + continue + } + + if iter.Key().String() == "geometries" { + collection := iter.Value().Interface() + items := reflect.ValueOf(collection) + + for j := 0; j < items.Len(); j++ { + shape, found := extractGeoShape(items.Index(j).Interface()) + if found { + f = found + rv = append(rv, shape) + } + } + } + } + } + + return rv, f +} + +// ExtractCircle takes an interface{} and tries it's best to +// interpret the center point coordinates and the radius for a +// given circle shape. +func ExtractCircle(thing interface{}) (*geojson.GeoShape, bool) { + thingVal := reflect.ValueOf(thing) + if !thingVal.IsValid() { + return nil, false + } + + rv := &geojson.GeoShape{ + Type: CircleType, + Center: make([]float64, 0, 2), + } + + if thingVal.Kind() == reflect.Map { + iter := thingVal.MapRange() + for iter.Next() { + + if iter.Key().String() == "radius" { + rv.Radius = iter.Value().Interface().(string) + continue + } + + if iter.Key().String() == "coordinates" { + lng, lat, found := ExtractGeoPoint(iter.Value().Interface()) + if !found { + return nil, false + } + rv.Center = append(rv.Center, lng, lat) + } + } + } + + return rv, true +} + +// ExtractGeoShapeCoordinates takes an interface{} and tries it's best to +// interpret the coordinates for any of the given geoshape typ like +// a point, multipoint, linestring, multilinestring, polygon, multipolygon, +func ExtractGeoShapeCoordinates(coordValue interface{}, + typ string) (*geojson.GeoShape, bool) { + rv := &geojson.GeoShape{ + Type: typ, + } + + if typ == PointType { + point := extractCoordinates(coordValue) + + // ignore the contents with invalid entry. + if len(point) < 2 { + return nil, false + } + + rv.Coordinates = [][][][]float64{{{point}}} + return rv, true + } + + if typ == MultiPointType || typ == LineStringType || + typ == EnvelopeType { + coords := extract2DCoordinates(coordValue) + + // ignore the contents with invalid entry. + if len(coords) == 0 { + return nil, false + } + + if typ == EnvelopeType && len(coords) != 2 { + return nil, false + } + + if typ == LineStringType && len(coords) < 2 { + return nil, false + } + + rv.Coordinates = [][][][]float64{{coords}} + return rv, true + } + + if typ == PolygonType || typ == MultiLineStringType { + coords := extract3DCoordinates(coordValue) + + // ignore the contents with invalid entry. + if len(coords) == 0 { + return nil, false + } + + if typ == PolygonType && len(coords[0]) < 3 || + typ == MultiLineStringType && len(coords[0]) < 2 { + return nil, false + } + + rv.Coordinates = [][][][]float64{coords} + return rv, true + } + + if typ == MultiPolygonType { + coords := extract4DCoordinates(coordValue) + + // ignore the contents with invalid entry. + if len(coords) == 0 || len(coords[0]) == 0 { + return nil, false + + } + + if len(coords[0][0]) < 3 { + return nil, false + } + + rv.Coordinates = coords + return rv, true + } + + return rv, false +} diff --git a/geo/parse_test.go b/geo/parse_test.go new file mode 100644 index 0000000..b32b55a --- /dev/null +++ b/geo/parse_test.go @@ -0,0 +1,478 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestExtractGeoPoint(t *testing.T) { + tests := []struct { + in interface{} + lon float64 + lat float64 + success bool + }{ + // values are ints + { + in: map[string]interface{}{ + "lat": 5, + "lon": 5, + }, + lon: 5, + lat: 5, + success: true, + }, + // values are uints + { + in: map[string]interface{}{ + "lat": uint(5), + "lon": uint(5), + }, + lon: 5, + lat: 5, + success: true, + }, + // values float64 as with parsed JSON + { + in: map[string]interface{}{ + "lat": 5.0, + "lon": 5.0, + }, + lon: 5, + lat: 5, + success: true, + }, + // values are bool (not supported) + { + in: map[string]interface{}{ + "lat": true, + "lon": false, + }, + lon: 0, + lat: 0, + success: false, + }, + // using lng variant of lon + { + in: map[string]interface{}{ + "lat": 5.0, + "lng": 5.0, + }, + lon: 5, + lat: 5, + success: true, + }, + // using struct + { + in: struct { + Lon float64 + Lat float64 + }{ + Lon: 3.0, + Lat: 7.5, + }, + lon: 3.0, + lat: 7.5, + success: true, + }, + // struct with lng alternate + { + in: struct { + Lng float64 + Lat float64 + }{ + Lng: 3.0, + Lat: 7.5, + }, + lon: 3.0, + lat: 7.5, + success: true, + }, + // test going throug interface + { + in: &s11{ + lon: 4.0, + lat: 6.9, + }, + lon: 4.0, + lat: 6.9, + success: true, + }, + // test going throug interface with lng variant + { + in: &s12{ + lng: 4.0, + lat: 6.9, + }, + lon: 4.0, + lat: 6.9, + success: true, + }, + // try GeoJSON slice + { + in: []interface{}{3.4, 5.9}, + lon: 3.4, + lat: 5.9, + success: true, + }, + // try GeoJSON slice too long + { + in: []interface{}{3.4, 5.9, 9.4}, + lon: 0, + lat: 0, + success: false, + }, + // slice of floats + { + in: []float64{3.4, 5.9}, + lon: 3.4, + lat: 5.9, + success: true, + }, + // values are nil (not supported) + { + in: map[string]interface{}{ + "lat": nil, + "lon": nil, + }, + lon: 0, + lat: 0, + success: false, + }, + // input is nil + { + in: nil, + lon: 0, + lat: 0, + success: false, + }, + } + + for _, test := range tests { + lon, lat, success := ExtractGeoPoint(test.in) + if success != test.success { + t.Errorf("expected extract geo point %t, got %t for %v", test.success, success, test.in) + } + if lon != test.lon { + t.Errorf("expected lon %f, got %f for %v", test.lon, lon, test.in) + } + if lat != test.lat { + t.Errorf("expected lat %f, got %f for %v", test.lat, lat, test.in) + } + } +} + +type s11 struct { + lon float64 + lat float64 +} + +func (s *s11) Lon() float64 { + return s.lon +} + +func (s *s11) Lat() float64 { + return s.lat +} + +type s12 struct { + lng float64 + lat float64 +} + +func (s *s12) Lng() float64 { + return s.lng +} + +func (s *s12) Lat() float64 { + return s.lat +} + +func TestExtractGeoShape(t *testing.T) { + tests := []struct { + in interface{} + resTyp string + coordinates [][][][]float64 + center []float64 + success bool + }{ + // valid point slice + { + in: map[string]interface{}{ + "coordinates": []interface{}{3.4, 5.9}, + "type": "Point", + }, + resTyp: "point", + coordinates: [][][][]float64{{{{3.4, 5.9}}}}, + success: true, + }, + // invalid point slice + { + in: map[string]interface{}{ + "coordinates": []interface{}{3.4}, + "type": "point"}, + + resTyp: "point", + coordinates: nil, + success: false, + }, + // valid multipoint slice containing single point + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 5.9}}, + "type": "multipoint"}, + resTyp: "multipoint", + coordinates: [][][][]float64{{{{3.4, 5.9}}}}, + success: true, + }, + // valid multipoint slice + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 5.9}, {6.7, 9.8}}, + "type": "multipoint"}, + resTyp: "multipoint", + coordinates: [][][][]float64{{{{3.4, 5.9}, {6.7, 9.8}}}}, + success: true, + }, + // valid multipoint slice containing one invalid entry + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 5.9}, {6.7}}, + "type": "multipoint"}, + resTyp: "multipoint", + coordinates: [][][][]float64{{{{3.4, 5.9}}}}, + success: true, + }, + // invalid multipoint slice + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4}}, + "type": "multipoint"}, + resTyp: "multipoint", + coordinates: nil, + success: false, + }, + // valid linestring slice + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}}, + "type": "linestring"}, + resTyp: "linestring", + coordinates: [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}}}}, + success: true, + }, + // valid linestring slice + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}, {10.1, 12.3}}, + "type": "linestring"}, + resTyp: "linestring", + coordinates: [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}, {10.1, 12.3}}}}, + success: true, + }, + // invalid linestring slice with single entry + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 4.4}}, + "type": "linestring"}, + resTyp: "linestring", + coordinates: nil, + success: false, + }, + // invalid linestring slice with wrong paranthesis + { + in: map[string]interface{}{ + "coordinates": [][][]interface{}{{{3.4, 4.4}, {8.4, 9.4}}}, + "type": "linestring"}, + resTyp: "linestring", + coordinates: nil, + success: false, + }, + // valid envelope + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 4.4}, {8.4, 9.4}}, + "type": "envelope"}, + resTyp: "envelope", + coordinates: [][][][]float64{{{{3.4, 4.4}, {8.4, 9.4}}}}, + success: true, + }, + // invalid envelope + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 4.4}}, + "type": "envelope"}, + resTyp: "envelope", + coordinates: nil, + success: false, + }, + // invalid envelope + { + in: map[string]interface{}{ + "coordinates": [][][]interface{}{{{3.4, 4.4}, {8.4, 9.4}}}, + "type": "envelope"}, + resTyp: "envelope", + coordinates: nil, + success: false, + }, + // invalid envelope with >2 vertices + { + in: map[string]interface{}{ + "coordinates": [][]interface{}{{3.4, 4.4}, {5.6, 6.4}, {7.4, 7.4}}, + "type": "envelope"}, + resTyp: "envelope", + coordinates: nil, + success: false, + }, + // valid circle + { + in: map[string]interface{}{ + "coordinates": []interface{}{4.4, 5.0}, + "radius": "200m", + "type": "circle"}, + resTyp: "circle", + center: []float64{4.4, 5.0}, + success: true, + }, + // invalid circle + { + in: map[string]interface{}{ + "coordinates": []interface{}{4.4}, + "radius": "200m", + "type": "circle"}, + resTyp: "circle", + success: false, + }, + } + + for _, test := range tests { + res, success := extractGeoShape(test.in) + if success != test.success { + t.Errorf("expected extract geo point: %t, got: %t for: %v", test.success, success, test.in) + } + if success && res.Type != test.resTyp { + t.Errorf("expected shape type: %v, got: %v for input: %v", test.resTyp, res.Type, test.in) + } + if success && !reflect.DeepEqual(test.coordinates, res.Coordinates) { + t.Errorf("expected result %+v, got %+v for %v", test.coordinates, res.Coordinates, test.in) + } + if success && test.center != nil && !reflect.DeepEqual(test.center, res.Center) { + t.Errorf("expected center %+v, got %+v for %v", test.center, res.Center, test.in) + } + } +} + +func TestExtractGeoShapeCoordinates(t *testing.T) { + tests := []struct { + x []byte + typ string + expectOK bool + }{ + { + x: []byte(`[ + [ + [77.58894681930542,12.976498523818783], + [77.58677959442139,12.974533005048169], + [77.58894681930542,12.976498523818783] + ] + ]`), + typ: PolygonType, + expectOK: true, + }, + { // Invalid construct, but handled + x: []byte(`[ + [ + {"lon":77.58894681930542,"lat":12.976498523818783}, + {"lon":77.58677959442139,"lat":12.974533005048169}, + {"lon":77.58894681930542,"lat":12.976498523818783} + ] + ]`), + typ: PolygonType, + expectOK: false, + }, + { // Invalid construct causes panic (within extract3DCoordinates), fix MB-65807 + x: []byte(`{ + "coordinates": [ + [77.58894681930542,12.976498523818783], + [77.58677959442139,12.974533005048169], + [77.58894681930542,12.976498523818783] + ] + }`), + typ: PolygonType, + expectOK: false, + }, + { + x: []byte(`[ + [ + [ + [-0.163421630859375,51.531600743186644], + [-0.15277862548828125,51.52455221546295], + [-0.15895843505859375,51.53693981046689], + [-0.163421630859375,51.531600743186644] + ] + ], + [ + [ + [-0.1902008056640625,51.5091698216777], + [-0.1599884033203125,51.51322956905176], + [-0.1902008056640625,51.5091698216777] + ] + ] + ]`), + typ: MultiPolygonType, + expectOK: true, + }, + { // Invalid construct causes panic (within extract3DCoordinates), fix MB-65807 + x: []byte(`[ + { + "coordinates": [ + [-0.163421630859375,51.531600743186644], + [-0.15277862548828125,51.52455221546295], + [-0.15895843505859375,51.53693981046689], + [-0.163421630859375,51.531600743186644] + ] + }, + { + "coordinates": [ + [-0.1902008056640625,51.5091698216777], + [-0.1599884033203125,51.51322956905176], + [-0.1902008056640625,51.5091698216777] + ] + } + ]`), + typ: MultiPolygonType, + expectOK: false, + }, + } + + for i := range tests { + var x interface{} + if err := json.Unmarshal(tests[i].x, &x); err != nil { + t.Fatalf("[%d] JSON err: %v", i+1, err) + } + + res, ok := ExtractGeoShapeCoordinates(x, tests[i].typ) + if ok != tests[i].expectOK { + t.Errorf("[%d] expected ok %t, got %t", i+1, tests[i].expectOK, ok) + } + + if ok && res.Type != tests[i].typ { + t.Errorf("[%d] expected type %s, got %s", i+1, tests[i].typ, res.Type) + } + } +} diff --git a/geo/sloppy.go b/geo/sloppy.go new file mode 100644 index 0000000..e9de06d --- /dev/null +++ b/geo/sloppy.go @@ -0,0 +1,59 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "math" +) + +var earthDiameterPerLatitude []float64 + +const ( + radiusTabsSize = (1 << 10) + 1 + radiusDelta = (math.Pi / 2) / (radiusTabsSize - 1) + radiusIndexer = 1 / radiusDelta +) + +func init() { + // initializes the tables used for the sloppy math functions + + // earth radius + a := 6378137.0 + b := 6356752.31420 + a2 := a * a + b2 := b * b + earthDiameterPerLatitude = make([]float64, radiusTabsSize) + earthDiameterPerLatitude[0] = 2.0 * a / 1000 + earthDiameterPerLatitude[radiusTabsSize-1] = 2.0 * b / 1000 + for i := 1; i < radiusTabsSize-1; i++ { + lat := math.Pi * float64(i) / (2*radiusTabsSize - 1) + one := math.Pow(a2*math.Cos(lat), 2) + two := math.Pow(b2*math.Sin(lat), 2) + three := math.Pow(float64(a)*math.Cos(lat), 2) + four := math.Pow(b*math.Sin(lat), 2) + radius := math.Sqrt((one + two) / (three + four)) + earthDiameterPerLatitude[i] = 2 * radius / 1000 + } +} + +// earthDiameter returns an estimation of the earth's diameter at the specified +// latitude in kilometers +func earthDiameter(lat float64) float64 { + index := math.Mod(math.Abs(lat)*radiusIndexer+0.5, float64(len(earthDiameterPerLatitude))) + if math.IsNaN(index) { + return 0 + } + return earthDiameterPerLatitude[int(index)] +} diff --git a/geo/versus_test.go b/geo/versus_test.go new file mode 100644 index 0000000..cac200c --- /dev/null +++ b/geo/versus_test.go @@ -0,0 +1,4156 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package geo + +import ( + "testing" +) + +// This test basically confirms the dimensions of the +// bounded box computed between the DecodeGeoHash method +// and the popular implementation from +// https://github.com/mmcloughlin/geohash. +// DecodeGeoHash method returns the centre of the rectangle +// than returning the box dimensions. +// This test verifies that the returned rectangle centre matches +// the centre for the box dimensions defined in the original +// implementation tests here: +// https://github.com/mmcloughlin/geohash/blob/master/decodecases_test.go +func TestDecodeGeoHashVersus(t *testing.T) { + tests := []struct { + hash string + box []float64 + }{ + {"91rc", []float64{7.20703125, 7.3828125, -124.1015625, -123.75}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"0fuz", []float64{-73.30078125, -73.125, -139.5703125, -139.21875}}, + {"dwfcndf", []float64{38.1596374512, 38.1610107422, -63.3444213867, -63.3430480957}}, + {"2z7", []float64{-4.21875, -2.8125, -142.03125, -140.625}}, + {"7spw2w", []float64{-21.3684082031, -21.3629150391, -11.9311523438, -11.9201660156}}, + {"eq", []float64{33.75, 39.375, -33.75, -22.5}}, + {"mgff0", []float64{-23.5546875, -23.5107421875, 82.6171875, 82.6611328125}}, + {"dp7k386jtk0", []float64{41.5306591988, 41.5306605399, -85.3607976437, -85.3607963026}}, + {"pjb", []float64{-57.65625, -56.25, 135.0, 136.40625}}, + {"jkc7uh9", []float64{-62.5973510742, -62.5959777832, 58.184967041, 58.186340332}}, + {"1gdp9", []float64{-68.994140625, -68.9501953125, -98.3935546875, -98.349609375}}, + {"z9yj14mmnxte", []float64{55.7359149121, 55.7359150797, 165.988941416, 165.988941751}}, + {"2brk", []float64{-42.890625, -42.71484375, -136.0546875, -135.703125}}, + {"dhv5t2qh59", []float64{27.3360496759, 27.3360550404, -82.7296471596, -82.7296364307}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"3fgd9k15b5k", []float64{-29.0691630542, -29.0691617131, -96.2718147039, -96.2718133628}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"b4", []float64{56.25, 61.875, -180.0, -168.75}}, + {"sb38", []float64{1.40625, 1.58203125, 35.859375, 36.2109375}}, + {"puqeug", []float64{-65.4180908203, -65.4125976562, 178.099365234, 178.110351562}}, + {"45", []float64{-73.125, -67.5, -90.0, -78.75}}, + {"34b", []float64{-29.53125, -28.125, -135.0, -133.59375}}, + {"tqb8jzn9dfqn", []float64{38.0074727163, 38.0074728839, 57.2148630023, 57.2148633376}}, + {"9x", []float64{39.375, 45.0, -112.5, -101.25}}, + {"tybf7", []float64{38.3642578125, 38.408203125, 79.9365234375, 79.98046875}}, + {"9nc", []float64{37.96875, 39.375, -133.59375, -132.1875}}, + {"pp21", []float64{-49.04296875, -48.8671875, 135.0, 135.3515625}}, + {"s6wjfu76v", []float64{15.0970602036, 15.0971031189, 19.8130273819, 19.8130702972}}, + {"wxh8ped7", []float64{39.3947410583, 39.3949127197, 119.160804749, 119.161148071}}, + {"8gr", []float64{18.28125, 19.6875, -136.40625, -135.0}}, + {"ug6hf", []float64{64.1162109375, 64.16015625, 36.650390625, 36.6943359375}}, + {"pb", []float64{-90.0, -84.375, 168.75, 180.0}}, + {"nmhvpv", []float64{-60.9686279297, -60.9631347656, 108.270263672, 108.28125}}, + {"rxgthm", []float64{-0.499877929688, -0.494384765625, 162.608642578, 162.619628906}}, + {"mj8t", []float64{-13.18359375, -13.0078125, 45.703125, 46.0546875}}, + {"rkvw", []float64{-17.2265625, -17.05078125, 153.984375, 154.3359375}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"u4ryw22k", []float64{58.8008880615, 58.8010597229, 11.1734390259, 11.1737823486}}, + {"96bf6jfr", []float64{15.8970451355, 15.8972167969, -122.60433197, -122.603988647}}, + {"ubhnbn2n1jvj", []float64{46.2219173647, 46.2219175324, 39.3750496209, 39.3750499561}}, + {"3gmczz", []float64{-26.3726806641, -26.3671875, -92.8234863281, -92.8125}}, + {"yb4jh3u7px0", []float64{45.8890718222, 45.8890731633, 126.75542593, 126.755427271}}, + {"9ex7rkbw1duf", []float64{20.2859266475, 20.2859268151, -101.985326596, -101.98532626}}, + {"xrkyg3mj", []float64{41.9754981995, 41.9756698608, 153.079376221, 153.079719543}}, + {"z4hn2yfzryjq", []float64{57.3869894072, 57.3869895749, 140.662075169, 140.662075505}}, + {"x357", []float64{6.15234375, 6.328125, 150.8203125, 151.171875}}, + {"v3pew", []float64{51.240234375, 51.2841796875, 67.060546875, 67.1044921875}}, + {"j1", []float64{-84.375, -78.75, 45.0, 56.25}}, + {"ec1bkph03", []float64{5.70744037628, 5.70748329163, -8.60774517059, -8.60770225525}}, + {"q4t85", []float64{-30.9375, -30.8935546875, 97.8662109375, 97.91015625}}, + {"k26z8hf", []float64{-42.2492980957, -42.2479248047, 15.119934082, 15.121307373}}, + {"p6fyq2u63", []float64{-73.4281110764, -73.428068161, 150.397725105, 150.397768021}}, + {"uqu5w2yu", []float64{83.5887908936, 83.5889625549, 17.1589279175, 17.1592712402}}, + {"terbkv", []float64{18.3526611328, 18.3581542969, 78.6071777344, 78.6181640625}}, + {"8v2nwkp", []float64{30.6958007812, 30.6971740723, -145.96572876, -145.964355469}}, + {"rbktxk2enhr", []float64{-42.6030693948, -42.6030680537, 175.397682041, 175.397683382}}, + {"r1pnfct8", []float64{-38.1802368164, -38.180065155, 144.97215271, 144.972496033}}, + {"rcmxg", []float64{-36.6064453125, -36.5625, 176.616210938, 176.66015625}}, + {"jqw7xjdt8", []float64{-52.7911090851, -52.7910661697, 65.350112915, 65.3501558304}}, + {"7mt737xd", []float64{-13.4716415405, -13.4714698792, -26.3019561768, -26.301612854}}, + {"2dprx5rg57es", []float64{-32.4132534117, -32.413253244, -146.986283138, -146.986282803}}, + {"fpr5d98ws0", []float64{86.40583992, 86.4058452845, -80.0455284119, -80.045517683}}, + {"z4cmm3", []float64{61.3970947266, 61.4025878906, 136.988525391, 136.999511719}}, + {"gz3b8j", []float64{85.8966064453, 85.9020996094, -8.7890625, -8.77807617188}}, + {"svfztv42f2k", []float64{33.6897052824, 33.6897066236, 37.8730648756, 37.8730662167}}, + {"z6f3yb6rum", []float64{60.7790976763, 60.7791030407, 149.713965654, 149.713976383}}, + {"wvqv397", []float64{30.4609680176, 30.4623413086, 133.312225342, 133.313598633}}, + {"r0ce65wshm", []float64{-40.1900213957, -40.1900160313, 137.206374407, 137.206385136}}, + {"crqvbtfbjb09", []float64{86.8235780485, 86.8235782161, -114.23181586, -114.231815524}}, + {"ptz0vc5z87", []float64{-57.5176173449, -57.5176119804, 167.601596117, 167.601606846}}, + {"x806byp", []float64{0.516357421875, 0.517730712891, 157.894134521, 157.895507812}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"kb23vjn10", []float64{-43.2584953308, -43.2584524155, 34.3295288086, 34.3295717239}}, + {"n82kv", []float64{-87.7587890625, -87.71484375, 113.071289062, 113.115234375}}, + {"dkhzmmqfg", []float64{23.8037252426, 23.803768158, -71.830201149, -71.8301582336}}, + {"y1v", []float64{54.84375, 56.25, 97.03125, 98.4375}}, + {"q977h9", []float64{-37.4359130859, -37.4304199219, 117.268066406, 117.279052734}}, + {"sdffzu5qzu", []float64{15.9753012657, 15.9753066301, 26.7125594616, 26.7125701904}}, + {"n3q", []float64{-82.96875, -81.5625, 109.6875, 111.09375}}, + {"fr99zgs7e", []float64{87.5149440765, 87.5149869919, -76.2940835953, -76.2940406799}}, + {"4d9cv4cbh", []float64{-75.6147766113, -75.614733696, -64.8167610168, -64.8167181015}}, + {"np896wq5", []float64{-47.557926178, -47.5577545166, 90.8212280273, 90.8215713501}}, + {"45", []float64{-73.125, -67.5, -90.0, -78.75}}, + {"c5srsy", []float64{66.0388183594, 66.0443115234, -128.814697266, -128.803710938}}, + {"vcshr", []float64{54.1845703125, 54.228515625, 84.6826171875, 84.7265625}}, + {"3zbbs1wnn5", []float64{-1.30907356739, -1.30906820297, -100.011034012, -100.011023283}}, + {"jtdvxn", []float64{-58.0627441406, -58.0572509766, 71.6748046875, 71.6857910156}}, + {"yz27wu0e", []float64{86.4189720154, 86.4191436768, 124.398880005, 124.399223328}}, + {"utb0ck65h9", []float64{77.4994522333, 77.4994575977, 22.5578713417, 22.5578820705}}, + {"1t1dvq5jj", []float64{-61.3577842712, -61.3577413559, -110.15557766, -110.155534744}}, + {"yzwgy5hvwz", []float64{87.8641408682, 87.8641462326, 133.512672186, 133.512682915}}, + {"8dx38y6vby", []float64{14.3615233898, 14.3615287542, -147.267919779, -147.26790905}}, + {"9bh", []float64{0.0, 1.40625, -95.625, -94.21875}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"x14ve", []float64{6.591796875, 6.6357421875, 138.999023438, 139.04296875}}, + {"3603v", []float64{-33.4423828125, -33.3984375, -123.178710938, -123.134765625}}, + {"bsuje4cn6", []float64{72.7017259598, 72.7017688751, -151.741704941, -151.741662025}}, + {"9nnut36epqhn", []float64{34.5484302565, 34.5484304242, -125.273349881, -125.273349546}}, + {"7q27jg", []float64{-9.29992675781, -9.29443359375, -33.1457519531, -33.134765625}}, + {"933zzd6", []float64{8.40591430664, 8.40728759766, -120.956726074, -120.955352783}}, + {"5743y", []float64{-72.8173828125, -72.7734375, -30.322265625, -30.2783203125}}, + {"7m94uc9", []float64{-13.5708618164, -13.5694885254, -32.1336364746, -32.1322631836}}, + {"780", []float64{-45.0, -43.59375, -22.5, -21.09375}}, + {"sqpqx2w0hh", []float64{34.8953461647, 34.8953515291, 21.7723274231, 21.7723381519}}, + {"1ep", []float64{-73.125, -71.71875, -102.65625, -101.25}}, + {"k0j", []float64{-45.0, -43.59375, 7.03125, 8.4375}}, + {"z1zgr0g440", []float64{55.4195022583, 55.4195076227, 146.210260391, 146.21027112}}, + {"681bf", []float64{-44.8681640625, -44.82421875, -64.951171875, -64.9072265625}}, + {"dn6j", []float64{36.03515625, 36.2109375, -87.1875, -86.8359375}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"t1m", []float64{7.03125, 8.4375, 52.03125, 53.4375}}, + {"9qkshh67xe3", []float64{35.8833391964, 35.8833405375, -117.242680639, -117.242679298}}, + {"4507gyj", []float64{-72.4328613281, -72.4314880371, -89.476776123, -89.475402832}}, + {"bb844h", []float64{48.1860351562, 48.1915283203, -146.162109375, -146.151123047}}, + {"trn6yz", []float64{39.8968505859, 39.90234375, 65.3356933594, 65.3466796875}}, + {"vb99", []float64{47.98828125, 48.1640625, 80.859375, 81.2109375}}, + {"dxpbksmed", []float64{39.4428920746, 39.4429349899, -56.3961696625, -56.3961267471}}, + {"zc", []float64{50.625, 56.25, 168.75, 180.0}}, + {"3webp3s6hdeb", []float64{-8.42890352011, -8.42890335247, -106.901924349, -106.901924014}}, + {"8qg", []float64{37.96875, 39.375, -164.53125, -163.125}}, + {"fgwzk", []float64{65.9619140625, 66.005859375, -46.58203125, -46.5380859375}}, + {"1q2mxvd", []float64{-53.8467407227, -53.8453674316, -123.055114746, -123.053741455}}, + {"sd", []float64{11.25, 16.875, 22.5, 33.75}}, + {"8hhwv", []float64{23.6865234375, 23.73046875, -173.452148438, -173.408203125}}, + {"1bw", []float64{-87.1875, -85.78125, -92.8125, -91.40625}}, + {"66zkh0ex724", []float64{-28.824133873, -28.8241325319, -68.3739575744, -68.3739562333}}, + {"w6qpy8", []float64{14.0185546875, 14.0240478516, 109.973144531, 109.984130859}}, + {"nux13fvfr", []float64{-64.4522809982, -64.4522380829, 133.678851128, 133.678894043}}, + {"cj2", []float64{74.53125, 75.9375, -135.0, -133.59375}}, + {"1qrumeqp", []float64{-54.0776252747, -54.0774536133, -112.601623535, -112.601280212}}, + {"hhm0", []float64{-66.09375, -65.91796875, 7.03125, 7.3828125}}, + {"50cp37u6w86", []float64{-84.4858060777, -84.4858047366, -43.5327002406, -43.5326988995}}, + {"0vjjfe5w", []float64{-60.8467483521, -60.8465766907, -139.1040802, -139.103736877}}, + {"xm67", []float64{30.05859375, 30.234375, 149.4140625, 149.765625}}, + {"1jmnqz1", []float64{-59.3316650391, -59.330291748, -127.67074585, -127.669372559}}, + {"jjfh69u78", []float64{-56.8989658356, -56.8989229202, 47.9281997681, 47.9282426834}}, + {"9xvnjtj3ytr", []float64{44.6762318909, 44.676233232, -105.219552666, -105.219551325}}, + {"ebwk44", []float64{3.52661132812, 3.53210449219, -2.373046875, -2.36206054688}}, + {"xej0p", []float64{16.875, 16.9189453125, 164.838867188, 164.8828125}}, + {"hm4m", []float64{-60.99609375, -60.8203125, 14.4140625, 14.765625}}, + {"71d1uhzq", []float64{-36.2277603149, -36.2275886536, -42.0017623901, -42.0014190674}}, + {"u7d8cgc9h4w", []float64{64.8401203752, 64.8401217163, 14.8447689414, 14.8447702825}}, + {"zwud", []float64{83.3203125, 83.49609375, 163.828125, 164.1796875}}, + {"p4nbh7wnwsb", []float64{-78.7296326458, -78.7296313047, 144.687473774, 144.687475115}}, + {"ev", []float64{28.125, 33.75, -11.25, 0.0}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"ytj8jhg3vmke", []float64{73.1514216028, 73.1514217705, 120.458796099, 120.458796434}}, + {"xn", []float64{33.75, 39.375, 135.0, 146.25}}, + {"1t7f", []float64{-60.1171875, -59.94140625, -107.2265625, -106.875}}, + {"wjt", []float64{30.9375, 32.34375, 97.03125, 98.4375}}, + {"f7j3", []float64{62.05078125, 62.2265625, -71.3671875, -71.015625}}, + {"62bd4mvcg", []float64{-40.3978013992, -40.3977584839, -77.9399728775, -77.9399299622}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"577epg1nh0be", []float64{-71.1738922633, -71.1738920957, -28.4860032052, -28.4860028699}}, + {"pvyz2qtjw6kc", []float64{-56.3451739959, -56.3451738283, 178.260314874, 178.26031521}}, + {"2hrf6fbj", []float64{-20.6822776794, -20.6821060181, -168.980712891, -168.980369568}}, + {"z1yz7p6dc3h8", []float64{56.1584669352, 56.1584671028, 144.627516344, 144.627516679}}, + {"24v5r460td50", []float64{-28.9475047588, -28.9475045912, -172.658146173, -172.658145837}}, + {"rdtnyvudc4mu", []float64{-29.7189060599, -29.7189058922, 164.834111296, 164.834111631}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"e7he", []float64{17.40234375, 17.578125, -27.421875, -27.0703125}}, + {"9yrkg", []float64{35.9912109375, 36.03515625, -90.9228515625, -90.87890625}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"yppvw0", []float64{85.341796875, 85.3472900391, 101.162109375, 101.173095703}}, + {"n3876wh", []float64{-80.9582519531, -80.9568786621, 101.716918945, 101.718292236}}, + {"m1vjqf4", []float64{-34.2224121094, -34.2210388184, 52.3306274414, 52.3320007324}}, + {"kev", []float64{-23.90625, -22.5, 29.53125, 30.9375}}, + {"4qc", []float64{-52.03125, -50.625, -77.34375, -75.9375}}, + {"5f4", []float64{-78.75, -77.34375, -8.4375, -7.03125}}, + {"ug", []float64{61.875, 67.5, 33.75, 45.0}}, + {"g5", []float64{61.875, 67.5, -45.0, -33.75}}, + {"7wvx", []float64{-5.80078125, -5.625, -14.765625, -14.4140625}}, + {"rx", []float64{-5.625, 0.0, 157.5, 168.75}}, + {"jdg0t21qzr", []float64{-74.4421631098, -74.4421577454, 71.9514906406, 71.9515013695}}, + {"606yg", []float64{-42.4072265625, -42.36328125, -86.0009765625, -85.95703125}}, + {"nxt6zwhgqd", []float64{-47.2955739498, -47.2955685854, 120.219204426, 120.219215155}}, + {"e71g7w8dr", []float64{17.482380867, 17.4824237823, -31.1342668533, -31.134223938}}, + {"n6u", []float64{-74.53125, -73.125, 106.875, 108.28125}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"zfcehd0d3", []float64{61.0074663162, 61.0075092316, 171.057858467, 171.057901382}}, + {"hgx5efc24r5e", []float64{-69.68212137, -69.6821212023, 43.760362789, 43.7603631243}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"q", []float64{-45.0, 0.0, 90.0, 135.0}}, + {"zbzg87qu07g", []float64{49.8525439203, 49.8525452614, 179.668708295, 179.668709636}}, + {"02urcn", []float64{-84.3859863281, -84.3804931641, -162.729492188, -162.718505859}}, + {"p4", []float64{-78.75, -73.125, 135.0, 146.25}}, + {"j6xjzcpsk78", []float64{-74.9205163121, -74.920514971, 66.4448082447, 66.4448095858}}, + {"svchwhvd08d", []float64{33.1612041593, 33.1612055004, 35.4274991155, 35.4275004566}}, + {"yughcgqek2", []float64{72.5721216202, 72.5721269846, 128.054763079, 128.054773808}}, + {"18tc1b", []float64{-87.01171875, -87.0062255859, -104.337158203, -104.326171875}}, + {"es43c", []float64{22.8076171875, 22.8515625, -19.2919921875, -19.248046875}}, + {"zdcephk", []float64{61.0194396973, 61.0208129883, 159.922485352, 159.923858643}}, + {"2zqh", []float64{-3.515625, -3.33984375, -137.8125, -137.4609375}}, + {"sh9bfh31", []float64{25.4678535461, 25.4680252075, 2.55020141602, 2.55054473877}}, + {"62vfgv", []float64{-40.2703857422, -40.2648925781, -70.4992675781, -70.48828125}}, + {"u2yn4", []float64{50.2734375, 50.3173828125, 19.775390625, 19.8193359375}}, + {"qx5wuf", []float64{-4.42749023438, -4.42199707031, 117.630615234, 117.641601562}}, + {"9y163", []float64{34.1455078125, 34.189453125, -99.4482421875, -99.404296875}}, + {"7ygu094y", []float64{-6.32160186768, -6.3214302063, -5.95081329346, -5.9504699707}}, + {"nkfj9vxh9e6", []float64{-62.2834508121, -62.283449471, 104.149084389, 104.14908573}}, + {"n220y9m", []float64{-88.4550476074, -88.4536743164, 101.542510986, 101.543884277}}, + {"472p7k4d", []float64{-70.4220199585, -70.4218482971, -78.6037445068, -78.6034011841}}, + {"7p", []float64{-5.625, 0.0, -45.0, -33.75}}, + {"b1r1n2zp", []float64{52.2123527527, 52.2125244141, -169.87197876, -169.871635437}}, + {"neu8fxsk8k4", []float64{-68.7324213982, -68.7324200571, 118.943838179, 118.94383952}}, + {"m33fn", []float64{-37.6171875, -37.5732421875, 58.974609375, 59.0185546875}}, + {"u6z5je", []float64{61.0125732422, 61.0180664062, 21.3354492188, 21.3464355469}}, + {"5e6csnf", []float64{-71.4179992676, -71.4166259766, -18.454284668, -18.452911377}}, + {"7f7k4", []float64{-31.640625, -31.5966796875, -6.591796875, -6.5478515625}}, + {"ykd07ytm4", []float64{70.3930091858, 70.3930521011, 104.23459053, 104.234633446}}, + {"ubyx97", []float64{50.5535888672, 50.5590820312, 42.9455566406, 42.9565429688}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"ungw16", []float64{84.0344238281, 84.0399169922, 4.97680664062, 4.98779296875}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"bhfkepk0n", []float64{72.5495910645, 72.5496339798, -176.698350906, -176.698307991}}, + {"d3", []float64{5.625, 11.25, -78.75, -67.5}}, + {"wsenn", []float64{26.3671875, 26.4111328125, 116.982421875, 117.026367188}}, + {"b4b6qpqk", []float64{60.9047698975, 60.9049415588, -179.376182556, -179.375839233}}, + {"zwjx2wv3", []float64{80.0616645813, 80.0618362427, 165.263557434, 165.263900757}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"3sgq6", []float64{-17.1826171875, -17.138671875, -107.841796875, -107.797851562}}, + {"9chbq", []float64{5.6689453125, 5.712890625, -94.306640625, -94.2626953125}}, + {"37nehnbt", []float64{-27.5597190857, -27.5595474243, -114.432907104, -114.432563782}}, + {"uhr5w71b", []float64{69.5379638672, 69.5381355286, 10.1208114624, 10.1211547852}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"81", []float64{5.625, 11.25, -180.0, -168.75}}, + {"vyxn", []float64{82.6171875, 82.79296875, 88.59375, 88.9453125}}, + {"73jvv", []float64{-38.3642578125, -38.3203125, -25.4443359375, -25.400390625}}, + {"nmw1ysg2f23", []float64{-58.7286601961, -58.728658855, 109.977705628, 109.977706969}}, + {"5sv6js2nphq", []float64{-62.9052887857, -62.9052874446, -14.8751798272, -14.8751784861}}, + {"289", []float64{-42.1875, -40.78125, -156.09375, -154.6875}}, + {"cq9", []float64{81.5625, 82.96875, -122.34375, -120.9375}}, + {"mm", []float64{-16.875, -11.25, 56.25, 67.5}}, + {"49378fm9v", []float64{-82.3408555984, -82.3408126831, -65.7014608383, -65.701417923}}, + {"ee079be", []float64{17.492980957, 17.494354248, -22.0674133301, -22.0660400391}}, + {"4m5cs3h", []float64{-61.6058349609, -61.6044616699, -73.2843017578, -73.2829284668}}, + {"hpdre", []float64{-46.494140625, -46.4501953125, 3.2958984375, 3.33984375}}, + {"06", []float64{-78.75, -73.125, -168.75, -157.5}}, + {"3x3r4evkpp1q", []float64{-2.9669566825, -2.96695651487, -110.624812357, -110.624812022}}, + {"95074fyh23t", []float64{17.4181875587, 17.4181888998, -134.51933071, -134.519329369}}, + {"5tdj7sf", []float64{-58.1135559082, -58.1121826172, -19.5309448242, -19.5295715332}}, + {"8r9z", []float64{43.41796875, 43.59375, -166.2890625, -165.9375}}, + {"nc", []float64{-84.375, -78.75, 123.75, 135.0}}, + {"t2mb", []float64{1.40625, 1.58203125, 64.3359375, 64.6875}}, + {"hcs1g2vbkq0g", []float64{-81.2506873347, -81.250687167, 39.525902085, 39.5259024203}}, + {"pduzpxt", []float64{-73.2595825195, -73.2582092285, 164.516143799, 164.51751709}}, + {"mw9u700", []float64{-7.6904296875, -7.68905639648, 70.0927734375, 70.0941467285}}, + {"9y0ttsfr", []float64{34.7440910339, 34.7442626953, -100.302085876, -100.301742554}}, + {"ztmu17de4", []float64{75.2541160583, 75.2541589737, 165.644388199, 165.644431114}}, + {"1dqmehx", []float64{-76.3522338867, -76.3508605957, -103.569488525, -103.568115234}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"6ydvkt6cp", []float64{-7.48563766479, -7.48559474945, -52.180981636, -52.1809387207}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"706tn", []float64{-42.71484375, -42.6708984375, -41.220703125, -41.1767578125}}, + {"6z89j2x0gun", []float64{-2.63382196426, -2.63382062316, -55.3063800931, -55.306378752}}, + {"mt1t", []float64{-15.99609375, -15.8203125, 69.609375, 69.9609375}}, + {"95ypdkpcd", []float64{22.4343395233, 22.4343824387, -126.452894211, -126.452851295}}, + {"nr1wc", []float64{-49.4384765625, -49.39453125, 103.403320312, 103.447265625}}, + {"rb2uxf4", []float64{-42.7917480469, -42.7903747559, 170.148010254, 170.149383545}}, + {"gzz5w20e1wk", []float64{89.2095328867, 89.2095342278, -1.13083541393, -1.13083407283}}, + {"mtuddkh7my", []float64{-12.1942341328, -12.1942287683, 73.9330852032, 73.933095932}}, + {"r15def4", []float64{-38.9245605469, -38.9231872559, 140.089416504, 140.090789795}}, + {"9fwcy0d", []float64{14.3728637695, 14.3742370605, -91.491394043, -91.490020752}}, + {"6vx9z80hkd", []float64{-13.7541425228, -13.7541371584, -45.3733420372, -45.3733313084}}, + {"qqxt55re", []float64{-7.54022598267, -7.54005432129, 111.93901062, 111.939353943}}, + {"4hreqy", []float64{-65.4895019531, -65.4840087891, -79.1564941406, -79.1455078125}}, + {"9nv6", []float64{38.3203125, 38.49609375, -127.6171875, -127.265625}}, + {"k980q", []float64{-36.5185546875, -36.474609375, 22.763671875, 22.8076171875}}, + {"zd6qmdhsrtce", []float64{58.7666300498, 58.7666302174, 160.912265405, 160.91226574}}, + {"ucz57k", []float64{55.4370117188, 55.4425048828, 43.7365722656, 43.7475585938}}, + {"u5pq", []float64{62.9296875, 63.10546875, 10.1953125, 10.546875}}, + {"fey0v9", []float64{66.2310791016, 66.2365722656, -58.8208007812, -58.8098144531}}, + {"mdtggek9fmh5", []float64{-30.2601397969, -30.2601396292, 75.7460278273, 75.7460281625}}, + {"y6nfy", []float64{56.7333984375, 56.77734375, 111.005859375, 111.049804688}}, + {"f", []float64{45.0, 90.0, -90.0, -45.0}}, + {"33hz5xbsw", []float64{-38.1011867523, -38.101143837, -116.915559769, -116.915516853}}, + {"2wnb71890np2", []float64{-11.1976110935, -11.1976109259, -147.875280194, -147.875279859}}, + {"7d", []float64{-33.75, -28.125, -22.5, -11.25}}, + {"71vsdbh", []float64{-34.365234375, -34.363861084, -37.1392822266, -37.1379089355}}, + {"nk", []float64{-67.5, -61.875, 101.25, 112.5}}, + {"pn6uks", []float64{-54.0747070312, -54.0692138672, 139.064941406, 139.075927734}}, + {"t0", []float64{0.0, 5.625, 45.0, 56.25}}, + {"ze7c4z3e", []float64{63.4973716736, 63.497543335, 162.896347046, 162.896690369}}, + {"ujq6txghfjdv", []float64{75.0141208805, 75.0141210482, 9.03497111052, 9.0349714458}}, + {"40muh9sfm7", []float64{-87.8819829226, -87.8819775581, -81.7095601559, -81.709549427}}, + {"0y4kk1", []float64{-55.4974365234, -55.4919433594, -142.91015625, -142.899169922}}, + {"q8btfscp1g", []float64{-39.7431975603, -39.7431921959, 113.314436674, 113.314447403}}, + {"yg2mxt", []float64{64.2755126953, 64.2810058594, 124.431152344, 124.442138672}}, + {"7r5kh", []float64{-4.921875, -4.8779296875, -29.00390625, -28.9599609375}}, + {"cvg6t", []float64{77.783203125, 77.8271484375, -96.4599609375, -96.416015625}}, + {"msj4q7e3db", []float64{-22.0850086212, -22.0850032568, 74.8104894161, 74.810500145}}, + {"1452n4", []float64{-78.7390136719, -78.7335205078, -130.166015625, -130.155029297}}, + {"xj3kvwr2d", []float64{30.4006290436, 30.4006719589, 137.009553909, 137.009596825}}, + {"2wtvb", []float64{-7.4267578125, -7.3828125, -149.4140625, -149.370117188}}, + {"c5x5dhk", []float64{65.3260803223, 65.3274536133, -125.062866211, -125.06149292}}, + {"eph56u6", []float64{39.9696350098, 39.9710083008, -39.2514038086, -39.2500305176}}, + {"5dhyg0hckf", []float64{-77.5632512569, -77.5632458925, -15.6817495823, -15.6817388535}}, + {"9vrb", []float64{29.53125, 29.70703125, -90.3515625, -90.0}}, + {"b7nsys", []float64{62.7319335938, 62.7374267578, -159.323730469, -159.312744141}}, + {"kmenbsk0", []float64{-12.8526306152, -12.8524589539, 15.4962158203, 15.4965591431}}, + {"j35", []float64{-84.375, -82.96875, 60.46875, 61.875}}, + {"e7sx", []float64{20.91796875, 21.09375, -27.421875, -27.0703125}}, + {"h87mpg", []float64{-87.6983642578, -87.6928710938, 27.4108886719, 27.421875}}, + {"qjbtp", []float64{-11.77734375, -11.7333984375, 91.0107421875, 91.0546875}}, + {"4zqs2pndx", []float64{-48.4327983856, -48.4327554703, -47.100148201, -47.1001052856}}, + {"1fsc5v3", []float64{-75.7328796387, -75.7315063477, -94.4041442871, -94.4027709961}}, + {"kp6hptx", []float64{-3.48541259766, -3.48403930664, 3.15170288086, 3.15307617188}}, + {"3n77", []float64{-9.31640625, -9.140625, -130.4296875, -130.078125}}, + {"q347uc", []float64{-38.7103271484, -38.7048339844, 104.622802734, 104.633789062}}, + {"n8gckvg3", []float64{-85.5297660828, -85.5295944214, 117.98664093, 117.986984253}}, + {"p7szbr6ceq", []float64{-68.9100801945, -68.9100748301, 152.944589853, 152.944600582}}, + {"8w7n", []float64{36.2109375, 36.38671875, -153.28125, -152.9296875}}, + {"k4s3ndj8", []float64{-30.7507324219, -30.7505607605, 6.26976013184, 6.27010345459}}, + {"fh38ev", []float64{69.0216064453, 69.0270996094, -87.7258300781, -87.71484375}}, + {"rzebrsw", []float64{-2.74383544922, -2.7424621582, 174.36126709, 174.362640381}}, + {"un", []float64{78.75, 84.375, 0.0, 11.25}}, + {"27u3d4ybbkt", []float64{-23.6273190379, -23.6273176968, -162.676259726, -162.676258385}}, + {"5hk2", []float64{-66.09375, -65.91796875, -39.0234375, -38.671875}}, + {"62f6wqsbfc6n", []float64{-40.3059548512, -40.3059546836, -75.3046354651, -75.3046351299}}, + {"r6jvqxczv", []float64{-32.7832460403, -32.783203125, 154.624199867, 154.624242783}}, + {"wyg1es5yx", []float64{38.2555103302, 38.2555532455, 128.128008842, 128.128051758}}, + {"smp9qbcpnt", []float64{28.3500748873, 28.3500802517, 22.0951581001, 22.095168829}}, + {"4q46h", []float64{-55.8984375, -55.8544921875, -75.41015625, -75.3662109375}}, + {"9u6v9g2ebcm5", []float64{24.8915505968, 24.8915507644, -97.3051826656, -97.3051823303}}, + {"25mwbs3", []float64{-25.5088806152, -25.5075073242, -172.242279053, -172.240905762}}, + {"kwf", []float64{-7.03125, -5.625, 25.3125, 26.71875}}, + {"ekqgkknev", []float64{24.5001554489, 24.5001983643, -24.0619039536, -24.0618610382}}, + {"y9974617cb12", []float64{53.9764738083, 53.9764739759, 114.358482845, 114.35848318}}, + {"htuu1sxcpd", []float64{-56.9282233715, -56.9282180071, 29.2565703392, 29.256581068}}, + {"150sv8q", []float64{-72.2886657715, -72.2872924805, -134.046936035, -134.045562744}}, + {"j36p6wnmqm", []float64{-81.6604489088, -81.6604435444, 59.181214571, 59.1812252998}}, + {"py", []float64{-56.25, -50.625, 168.75, 180.0}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"y87", []float64{46.40625, 47.8125, 116.71875, 118.125}}, + {"6v90bwn999", []float64{-13.8974422216, -13.8974368572, -54.8127865791, -54.8127758503}}, + {"8kyq", []float64{27.7734375, 27.94921875, -159.9609375, -159.609375}}, + {"8cht765b92zg", []float64{6.55892824754, 6.55892841518, -139.773838855, -139.77383852}}, + {"nu5jwk23v7f", []float64{-66.5095366538, -66.5095353127, 128.243979514, 128.243980855}}, + {"0m10969", []float64{-61.7733764648, -61.7720031738, -167.287445068, -167.286071777}}, + {"s29h49frdp", []float64{3.52656304836, 3.52656841278, 12.7692890167, 12.7692997456}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"brmmeg2z8r", []float64{86.7672246695, 86.7672300339, -161.201351881, -161.201341152}}, + {"r3bngg619d", []float64{-33.9516055584, -33.951600194, 146.417605877, 146.417616606}}, + {"rmz76wced8c", []float64{-12.0472772419, -12.0472759008, 156.557344347, 156.557345688}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"2h", []float64{-22.5, -16.875, -180.0, -168.75}}, + {"ty2764x", []float64{35.7412719727, 35.7426452637, 79.1990661621, 79.2004394531}}, + {"5yh3330r", []float64{-56.0235786438, -56.0234069824, -5.21816253662, -5.21781921387}}, + {"9szz", []float64{27.94921875, 28.125, -101.6015625, -101.25}}, + {"x7d41b", []float64{20.0390625, 20.0445556641, 149.139404297, 149.150390625}}, + {"dw", []float64{33.75, 39.375, -67.5, -56.25}}, + {"gnd4cw", []float64{82.0788574219, 82.0843505859, -42.1215820312, -42.1105957031}}, + {"k9bxc2n8", []float64{-33.7939453125, -33.7937736511, 23.2669830322, 23.267326355}}, + {"hump4nk", []float64{-64.8289489746, -64.8275756836, 40.8746337891, 40.8760070801}}, + {"gkz", []float64{71.71875, 73.125, -23.90625, -22.5}}, + {"g9e08yt", []float64{53.5610961914, 53.5624694824, -18.2414245605, -18.2400512695}}, + {"3eyuyzpm", []float64{-23.0319786072, -23.0318069458, -102.701225281, -102.700881958}}, + {"utpuc59p4m", []float64{73.9804154634, 73.9804208279, 33.443852663, 33.4438633919}}, + {"cnqt", []float64{81.03515625, 81.2109375, -125.859375, -125.5078125}}, + {"z05xfy72gk0", []float64{46.3967871666, 46.3967885077, 140.04732728, 140.047328621}}, + {"skr4zd", []float64{24.4006347656, 24.4061279297, 21.4233398438, 21.4343261719}}, + {"h2fe8mdnq", []float64{-85.1347303391, -85.1346874237, 14.7796154022, 14.7796583176}}, + {"z18b", []float64{53.4375, 53.61328125, 136.0546875, 136.40625}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"vh", []float64{67.5, 73.125, 45.0, 56.25}}, + {"v64zfspngxw", []float64{57.6354762912, 57.6354776323, 60.2368220687, 60.2368234098}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"d800", []float64{0.0, 0.17578125, -67.5, -67.1484375}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"bnugeje1", []float64{83.6143684387, 83.6145401001, -173.184356689, -173.184013367}}, + {"46", []float64{-78.75, -73.125, -78.75, -67.5}}, + {"8rsncsbz2mx4", []float64{43.4013903514, 43.401390519, -163.058031946, -163.058031611}}, + {"t4w9wh7f98je", []float64{14.3499474786, 14.3499476463, 54.4095184654, 54.4095188007}}, + {"nsr1tp2", []float64{-65.7902526855, -65.7888793945, 122.563476562, 122.564849854}}, + {"9me", []float64{30.9375, 32.34375, -119.53125, -118.125}}, + {"t8250bh2y", []float64{1.93372249603, 1.93376541138, 67.5390529633, 67.5390958786}}, + {"1", []float64{-90.0, -45.0, -135.0, -90.0}}, + {"uqty0nmvvj0c", []float64{82.652533818, 82.6525339857, 19.3440495059, 19.3440498412}}, + {"7nkxt", []float64{-8.525390625, -8.4814453125, -38.4521484375, -38.408203125}}, + {"jzev", []float64{-46.93359375, -46.7578125, 84.0234375, 84.375}}, + {"dmtj1fktxe", []float64{31.8297261, 31.8297314644, -71.6353440285, -71.6353332996}}, + {"0r", []float64{-50.625, -45.0, -168.75, -157.5}}, + {"5hqv273y", []float64{-65.152015686, -65.1518440247, -35.4944229126, -35.4940795898}}, + {"78k", []float64{-43.59375, -42.1875, -16.875, -15.46875}}, + {"f2krt5", []float64{47.7410888672, 47.7465820312, -72.5537109375, -72.5427246094}}, + {"ffw63hhzm2u", []float64{59.481229037, 59.4812303782, -47.4102383852, -47.4102370441}}, + {"mv3z5k", []float64{-14.2163085938, -14.2108154297, 81.3537597656, 81.3647460938}}, + {"f15", []float64{50.625, 52.03125, -85.78125, -84.375}}, + {"j710re25dhzs", []float64{-73.0625749379, -73.0625747703, 57.9859357327, 57.985936068}}, + {"rtt328k93", []float64{-13.8411855698, -13.8411426544, 164.911007881, 164.911050797}}, + {"d2x003t048", []float64{2.82073974609, 2.82074511051, -68.8882899284, -68.8882791996}}, + {"22uy0", []float64{-39.7265625, -39.6826171875, -162.0703125, -162.026367188}}, + {"tuzxx", []float64{28.037109375, 28.0810546875, 89.6044921875, 89.6484375}}, + {"su44fdqr2pv1", []float64{22.9970443435, 22.9970445111, 36.6809530556, 36.6809533909}}, + {"yttq", []float64{76.9921875, 77.16796875, 119.8828125, 120.234375}}, + {"9fu5vyr", []float64{16.1622619629, 16.1636352539, -95.362701416, -95.361328125}}, + {"zwmzk37wsh97", []float64{81.4386709593, 81.438671127, 165.777684934, 165.77768527}}, + {"hd777ygzewj", []float64{-76.7340624332, -76.7340610921, 27.2404141724, 27.2404155135}}, + {"0b1", []float64{-90.0, -88.59375, -144.84375, -143.4375}}, + {"ejmgd", []float64{30.146484375, 30.1904296875, -36.826171875, -36.7822265625}}, + {"rzxh6", []float64{-2.0654296875, -2.021484375, 178.681640625, 178.725585938}}, + {"0rf4f4u", []float64{-45.9077453613, -45.9063720703, -165.844116211, -165.84274292}}, + {"1m23ggbv8", []float64{-60.1395893097, -60.1395463943, -123.23261261, -123.232569695}}, + {"gdvt2y6wfv", []float64{61.4271193743, 61.4271247387, -14.7291147709, -14.7291040421}}, + {"wxb3eqeug0y0", []float64{43.8939468563, 43.8939470239, 112.9996714, 112.999671735}}, + {"516kngbm6", []float64{-82.2441244125, -82.2440814972, -41.5388774872, -41.5388345718}}, + {"xtuwe2zu", []float64{33.4911346436, 33.4913063049, 163.981590271, 163.981933594}}, + {"dhb1c2jrz6c1", []float64{27.027712483, 27.0277126506, -89.9375461042, -89.9375457689}}, + {"23c397n4v8g", []float64{-34.8756225407, -34.8756211996, -166.928776056, -166.928774714}}, + {"1w81bnmc3", []float64{-53.0953359604, -53.095293045, -112.492060661, -112.492017746}}, + {"03hu77r", []float64{-83.6100769043, -83.6087036133, -161.917877197, -161.916503906}}, + {"z2vrm", []float64{50.4931640625, 50.537109375, 153.852539062, 153.896484375}}, + {"q630e", []float64{-32.255859375, -32.2119140625, 102.788085938, 102.83203125}}, + {"h9uzt", []float64{-78.837890625, -78.7939453125, 29.3994140625, 29.443359375}}, + {"x09c9jz", []float64{3.10775756836, 3.10913085938, 137.51449585, 137.515869141}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"xv9ee3gq2j", []float64{31.5634471178, 31.5634524822, 171.006660461, 171.00667119}}, + {"6e8xjp4", []float64{-24.0435791016, -24.0422058105, -66.5744018555, -66.5730285645}}, + {"0m8sj439bys", []float64{-58.3466801047, -58.3466787636, -167.82505095, -167.825049609}}, + {"khf7yq8js6", []float64{-17.5854098797, -17.5854045153, 3.43890309334, 3.43891382217}}, + {"hh", []float64{-67.5, -61.875, 0.0, 11.25}}, + {"kcyx9b0rd65e", []float64{-33.8365919329, -33.8365917653, 42.967973873, 42.9679742083}}, + {"qcy4pees", []float64{-34.7847747803, -34.7846031189, 132.521896362, 132.522239685}}, + {"tc6", []float64{7.03125, 8.4375, 81.5625, 82.96875}}, + {"mxhnk2fkh", []float64{-4.52156066895, -4.5215177536, 73.3150291443, 73.3150720596}}, + {"1mggmg5x0k", []float64{-57.067258358, -57.0672529936, -118.219059706, -118.219048977}}, + {"f1udt102z", []float64{55.2888250351, 55.2888679504, -83.4515047073, -83.451461792}}, + {"jjz", []float64{-57.65625, -56.25, 54.84375, 56.25}}, + {"1q1dg8peze2", []float64{-55.765940398, -55.7659390569, -121.476194859, -121.476193517}}, + {"604unch", []float64{-44.2913818359, -44.2900085449, -85.8306884766, -85.8293151855}}, + {"kt", []float64{-16.875, -11.25, 22.5, 33.75}}, + {"wpbgc", []float64{44.2529296875, 44.296875, 91.0986328125, 91.142578125}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"tz2pky3c", []float64{42.0901679993, 42.0903396606, 78.9611434937, 78.9614868164}}, + {"j96v3y", []float64{-82.0129394531, -82.0074462891, 71.4440917969, 71.455078125}}, + {"vs974dv", []float64{70.8549499512, 70.8563232422, 69.3745422363, 69.3759155273}}, + {"dunwchdt7d6", []float64{23.712155968, 23.7121573091, -47.061843574, -47.0618422329}}, + {"h2u2nkksw", []float64{-85.7571315765, -85.7570886612, 17.5076580048, 17.5077009201}}, + {"z8qtu1", []float64{47.4224853516, 47.4279785156, 166.81640625, 166.827392578}}, + {"2677dsdngh", []float64{-31.7026162148, -31.7026108503, -164.066948891, -164.066938162}}, + {"4yy", []float64{-52.03125, -50.625, -47.8125, -46.40625}}, + {"uym", []float64{80.15625, 81.5625, 40.78125, 42.1875}}, + {"m2mssd", []float64{-42.7917480469, -42.7862548828, 64.1821289062, 64.1931152344}}, + {"xed3b", []float64{19.9951171875, 20.0390625, 160.6640625, 160.708007812}}, + {"rfp4ky", []float64{-33.3215332031, -33.3160400391, 178.802490234, 178.813476562}}, + {"83fmm9e", []float64{10.7748413086, 10.7762145996, -165.340118408, -165.338745117}}, + {"tr7z2dqh", []float64{42.0687103271, 42.0688819885, 61.5536499023, 61.5539932251}}, + {"crsh", []float64{87.890625, 88.06640625, -118.125, -117.7734375}}, + {"f", []float64{45.0, 90.0, -90.0, -45.0}}, + {"761m6h", []float64{-32.8051757812, -32.7996826172, -31.904296875, -31.8933105469}}, + {"7p", []float64{-5.625, 0.0, -45.0, -33.75}}, + {"8qu9r5", []float64{38.2049560547, 38.2104492188, -162.114257812, -162.103271484}}, + {"5z", []float64{-50.625, -45.0, -11.25, 0.0}}, + {"kbz", []float64{-40.78125, -39.375, 43.59375, 45.0}}, + {"zdftsw1rbpsf", []float64{61.4698768035, 61.4698769711, 161.21510189, 161.215102226}}, + {"n1m105r", []float64{-82.7751159668, -82.7737426758, 97.0408630371, 97.0422363281}}, + {"xf4kktnxsz", []float64{12.0258611441, 12.0258665085, 172.120946646, 172.120957375}}, + {"kzqyq4m0qtyu", []float64{-3.10768313706, -3.10768296942, 43.5130138323, 43.5130141675}}, + {"1j1vy57w7", []float64{-60.8453321457, -60.8452892303, -132.27045536, -132.270412445}}, + {"nq4u3", []float64{-55.5029296875, -55.458984375, 105.161132812, 105.205078125}}, + {"bhcy", []float64{72.7734375, 72.94921875, -177.5390625, -177.1875}}, + {"vjr", []float64{74.53125, 75.9375, 54.84375, 56.25}}, + {"uc99nrdsntv", []float64{53.6551974714, 53.6551988125, 36.1377520859, 36.137753427}}, + {"3e8zmnz", []float64{-24.0010070801, -23.9996337891, -111.2159729, -111.214599609}}, + {"j0yzdvs9", []float64{-84.4325065613, -84.4323348999, 54.6192169189, 54.6195602417}}, + {"8chvm4", []float64{6.55883789062, 6.56433105469, -139.350585938, -139.339599609}}, + {"ywf6099wx", []float64{83.329668045, 83.3297109604, 115.6883955, 115.688438416}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"zrc", []float64{88.59375, 90.0, 147.65625, 149.0625}}, + {"zq", []float64{78.75, 84.375, 146.25, 157.5}}, + {"7xznu50ru", []float64{-0.201916694641, -0.201873779297, -12.4799537659, -12.4799108505}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"7r", []float64{-5.625, 0.0, -33.75, -22.5}}, + {"f27k", []float64{47.109375, 47.28515625, -74.1796875, -73.828125}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"b4s13188yv", []float64{59.2906218767, 59.2906272411, -174.330078363, -174.330067635}}, + {"q8", []float64{-45.0, -39.375, 112.5, 123.75}}, + {"skn7w", []float64{23.115234375, 23.1591796875, 20.302734375, 20.3466796875}}, + {"1tzqwzyh5vp", []float64{-56.4703863859, -56.4703850448, -101.999646574, -101.999645233}}, + {"r52cmn", []float64{-26.4660644531, -26.4605712891, 136.274414062, 136.285400391}}, + {"7bwvbbbru", []float64{-41.1713075638, -41.1712646484, -1.72433853149, -1.72429561615}}, + {"ruv0b1n", []float64{-18.1439208984, -18.1425476074, 175.789489746, 175.790863037}}, + {"vwf01ujm", []float64{82.9915809631, 82.9917526245, 70.3966140747, 70.3969573975}}, + {"0metgxjtrm91", []float64{-58.0123747699, -58.0123746023, -163.666450828, -163.666450493}}, + {"2w", []float64{-11.25, -5.625, -157.5, -146.25}}, + {"8kmch3", []float64{24.0875244141, 24.0930175781, -160.477294922, -160.466308594}}, + {"g6m", []float64{57.65625, 59.0625, -26.71875, -25.3125}}, + {"t6v4k2", []float64{15.8642578125, 15.8697509766, 63.4680175781, 63.4790039062}}, + {"zr02vfju8hd", []float64{84.5186188817, 84.5186202228, 146.862147152, 146.862148494}}, + {"kb8jn751", []float64{-41.2919425964, -41.2917709351, 34.0287780762, 34.0291213989}}, + {"mj76", []float64{-15.1171875, -14.94140625, 49.5703125, 49.921875}}, + {"f0hwcbkwjnr", []float64{46.1889602244, 46.1889615655, -83.5885669291, -83.588565588}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"6rt9btpxj2p1", []float64{-2.47621519491, -2.47621502727, -70.9831179678, -70.9831176326}}, + {"wh0s6yb6j", []float64{23.2844924927, 23.284535408, 90.8245325089, 90.8245754242}}, + {"65b", []float64{-23.90625, -22.5, -90.0, -88.59375}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"z0tpc5d", []float64{49.1940307617, 49.1954040527, 142.077941895, 142.079315186}}, + {"fgs2w0", []float64{64.775390625, 64.7808837891, -50.009765625, -49.9987792969}}, + {"vfus35qgp", []float64{61.2341880798, 61.2342309952, 85.1316404343, 85.1316833496}}, + {"hywnkqdye", []float64{-52.3020458221, -52.3020029068, 42.3781728745, 42.3782157898}}, + {"qpwxwun0b", []float64{-1.47203922272, -1.47199630737, 99.4454956055, 99.4455385208}}, + {"v88u9rqr24", []float64{48.6445963383, 48.6446017027, 68.6182022095, 68.6182129383}}, + {"17x06x4fyw", []float64{-70.2295982838, -70.2295929193, -113.792331219, -113.79232049}}, + {"3ykkhjyhrt", []float64{-9.1082829237, -9.10827755928, -95.0890946388, -95.08908391}}, + {"mbzkuqw5", []float64{-39.910068512, -39.9098968506, 89.1403198242, 89.140663147}}, + {"yqjf0p3mn", []float64{79.1422462463, 79.1422891617, 109.337911606, 109.337954521}}, + {"2f", []float64{-33.75, -28.125, -146.25, -135.0}}, + {"hqsm3", []float64{-52.5146484375, -52.470703125, 17.2705078125, 17.314453125}}, + {"gp1y4wtft1tp", []float64{85.4658314399, 85.4658316076, -42.4210815132, -42.4210811779}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"kxxruvh", []float64{-1.42272949219, -1.42135620117, 32.9095458984, 32.9109191895}}, + {"08h0ft4vk97r", []float64{-89.839789141, -89.8397889733, -151.761162691, -151.761162356}}, + {"y9u8", []float64{54.84375, 55.01953125, 118.828125, 119.1796875}}, + {"3zhsk", []float64{-4.8779296875, -4.833984375, -94.74609375, -94.7021484375}}, + {"x2", []float64{0.0, 5.625, 146.25, 157.5}}, + {"mr7nnvr", []float64{-3.13522338867, -3.13385009766, 60.7749938965, 60.7763671875}}, + {"d3g0pn54hr", []float64{9.87708985806, 9.87709522247, -74.2193305492, -74.2193198204}}, + {"jv173u", []float64{-61.2817382812, -61.2762451172, 80.5847167969, 80.595703125}}, + {"vte", []float64{75.9375, 77.34375, 71.71875, 73.125}}, + {"303un61j2s", []float64{-42.878715992, -42.8787106276, -132.263009548, -132.262998819}}, + {"m528h", []float64{-26.71875, -26.6748046875, 45.87890625, 45.9228515625}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"p8h", []float64{-90.0, -88.59375, 163.125, 164.53125}}, + {"tzusgm6v33y", []float64{44.4584606588, 44.4584619999, 85.2247855067, 85.2247868478}}, + {"26sre", []float64{-29.619140625, -29.5751953125, -162.641601562, -162.59765625}}, + {"q1tcnkmh55b", []float64{-36.3626660407, -36.3626646996, 98.3675909042, 98.3675922453}}, + {"xs4t1g3", []float64{23.3967590332, 23.3981323242, 161.093902588, 161.095275879}}, + {"td", []float64{11.25, 16.875, 67.5, 78.75}}, + {"xdvwxsmsjd", []float64{16.6353714466, 16.635376811, 165.571753979, 165.571764708}}, + {"dnw", []float64{36.5625, 37.96875, -81.5625, -80.15625}}, + {"u7nu2ff43vx", []float64{62.6375922561, 62.6375935972, 20.777977556, 20.7779788971}}, + {"0jy2", []float64{-57.65625, -57.48046875, -171.2109375, -170.859375}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"2yx2k", []float64{-8.3935546875, -8.349609375, -135.87890625, -135.834960938}}, + {"g3", []float64{50.625, 56.25, -33.75, -22.5}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"vrrpw34jfjs", []float64{87.1061190963, 87.1061204374, 66.3712459803, 66.3712473214}}, + {"g2gkzeutg", []float64{50.0752973557, 50.075340271, -28.8437891006, -28.8437461853}}, + {"z8ett4zh", []float64{48.7950897217, 48.7952613831, 162.6512146, 162.651557922}}, + {"cyspv2k0", []float64{82.9261779785, 82.9263496399, -95.3887939453, -95.3884506226}}, + {"fqu7qbshmr", []float64{83.5435527563, 83.5435581207, -72.471088171, -72.4710774422}}, + {"578u4ww7", []float64{-69.5731544495, -69.5729827881, -32.5768661499, -32.5765228271}}, + {"4gzkwxrzb", []float64{-68.0740785599, -68.0740356445, -45.7583999634, -45.758357048}}, + {"g0z038npm1ym", []float64{49.2639500834, 49.263950251, -35.0818693265, -35.0818689913}}, + {"vrse4ey62b1y", []float64{87.7358303592, 87.7358305268, 62.6966058835, 62.6966062188}}, + {"7x2fkbbj6", []float64{-3.81822109222, -3.81817817688, -21.2364864349, -21.2364435196}}, + {"tuvfdxy7b", []float64{27.2014188766, 27.201461792, 86.9543838501, 86.9544267654}}, + {"9qzggb", []float64{38.6279296875, 38.6334228516, -112.686767578, -112.67578125}}, + {"pupf6", []float64{-67.1044921875, -67.060546875, 179.736328125, 179.780273438}}, + {"fknyrbe41k0w", []float64{68.6017451808, 68.6017453484, -68.9130621403, -68.9130618051}}, + {"591vk5jz7x4v", []float64{-83.4343860112, -83.4343858436, -19.8552309349, -19.8552305996}}, + {"n4psdv", []float64{-77.9315185547, -77.9260253906, 100.667724609, 100.678710938}}, + {"zyw2", []float64{81.5625, 81.73828125, 177.5390625, 177.890625}}, + {"r0e29", []float64{-42.099609375, -42.0556640625, 139.614257812, 139.658203125}}, + {"j9cq7f", []float64{-79.0466308594, -79.0411376953, 69.4226074219, 69.43359375}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"m42x20h", []float64{-31.0693359375, -31.0679626465, 45.7086181641, 45.7099914551}}, + {"405zg3kz2j", []float64{-88.6295574903, -88.6295521259, -84.5772171021, -84.5772063732}}, + {"7vu7httsdm", []float64{-12.0978945494, -12.097889185, -5.06803393364, -5.0680232048}}, + {"prznz91", []float64{-45.2142333984, -45.2128601074, 156.424713135, 156.426086426}}, + {"46dupvqwbsuj", []float64{-75.2043508552, -75.2043506876, -74.5332831144, -74.5332827792}}, + {"7s0", []float64{-22.5, -21.09375, -22.5, -21.09375}}, + {"5nhz5er2nnm5", []float64{-55.0016444363, -55.0016442686, -38.1562833488, -38.1562830135}}, + {"5qqp", []float64{-53.61328125, -53.4375, -25.3125, -24.9609375}}, + {"t4dqn2", []float64{15.1171875, 15.1226806641, 48.4387207031, 48.4497070312}}, + {"gmxt", []float64{76.81640625, 76.9921875, -23.203125, -22.8515625}}, + {"syuh0ke1syh", []float64{38.6968839169, 38.696885258, 39.3903154135, 39.3903167546}}, + {"nvr0", []float64{-60.46875, -60.29296875, 133.59375, 133.9453125}}, + {"g4", []float64{56.25, 61.875, -45.0, -33.75}}, + {"gnd2kb6w0s32", []float64{81.6088713706, 81.6088715382, -41.623740904, -41.6237405688}}, + {"x7hptvsgdvu", []float64{18.2242034376, 18.2242047787, 152.134332061, 152.134333402}}, + {"2fc8", []float64{-29.53125, -29.35546875, -144.140625, -143.7890625}}, + {"e3fsu48yte7", []float64{10.693577081, 10.6935784221, -30.057323724, -30.0573223829}}, + {"pbqhjvqh", []float64{-87.8610992432, -87.8609275818, 177.448425293, 177.448768616}}, + {"sqx1nwgn", []float64{36.7763900757, 36.7765617371, 21.3835144043, 21.3838577271}}, + {"e6g", []float64{15.46875, 16.875, -29.53125, -28.125}}, + {"dt", []float64{28.125, 33.75, -67.5, -56.25}}, + {"02s64yzk", []float64{-86.7981719971, -86.7980003357, -162.642631531, -162.642288208}}, + {"z9", []float64{50.625, 56.25, 157.5, 168.75}}, + {"fjv", []float64{77.34375, 78.75, -82.96875, -81.5625}}, + {"5yc0", []float64{-52.03125, -51.85546875, -9.84375, -9.4921875}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"k3x3j1pmdnfp", []float64{-36.3802440651, -36.3802438974, 21.6750839353, 21.6750842705}}, + {"g2pjds", []float64{45.9887695312, 45.9942626953, -23.7963867188, -23.7854003906}}, + {"ppw0p3q2gzbk", []float64{-47.8054625541, -47.8054623865, 143.764847852, 143.764848188}}, + {"9xwp0kprs0x6", []float64{43.4412318841, 43.4412320517, -104.041375928, -104.041375592}}, + {"0gzww7fv7gj", []float64{-67.7421551943, -67.7421538532, -135.424522609, -135.424521267}}, + {"sgpt2", []float64{17.7978515625, 17.841796875, 44.296875, 44.3408203125}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"rr", []float64{-5.625, 0.0, 146.25, 157.5}}, + {"wkqrtvj5", []float64{25.2525901794, 25.2527618408, 110.298614502, 110.298957825}}, + {"ngtfyx77u", []float64{-69.7886323929, -69.7885894775, 132.126216888, 132.126259804}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"58y", []float64{-85.78125, -84.375, -14.0625, -12.65625}}, + {"mvjrxvx", []float64{-15.5264282227, -15.5250549316, 86.483001709, 86.484375}}, + {"jgve4ttu", []float64{-68.3480072021, -68.3478355408, 86.6021347046, 86.6024780273}}, + {"h9", []float64{-84.375, -78.75, 22.5, 33.75}}, + {"ytf9sb6mj27", []float64{77.609654814, 77.6096561551, 116.227684468, 116.227685809}}, + {"rk9kv3q", []float64{-18.8456726074, -18.8442993164, 148.246765137, 148.248138428}}, + {"kbq8t", []float64{-43.505859375, -43.4619140625, 43.1103515625, 43.154296875}}, + {"j8prqp8kx", []float64{-88.6836147308, -88.6835718155, 77.9596281052, 77.9596710205}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"mq53k", []float64{-11.0302734375, -10.986328125, 60.99609375, 61.0400390625}}, + {"d95fw", []float64{6.064453125, 6.1083984375, -61.962890625, -61.9189453125}}, + {"x15j3", []float64{6.5478515625, 6.591796875, 139.262695312, 139.306640625}}, + {"x0yg7k2b", []float64{4.81338500977, 4.81355667114, 144.636039734, 144.636383057}}, + {"dx8x9yxwprk5", []float64{43.5426343046, 43.5426344723, -66.7093545198, -66.7093541846}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"429ptwcscc3", []float64{-85.8312396705, -85.8312383294, -77.0999144018, -77.0999130607}}, + {"ncknjt", []float64{-81.8865966797, -81.8811035156, 129.616699219, 129.627685547}}, + {"7ce8p70yc7h", []float64{-36.5448457003, -36.5448443592, -6.00843250751, -6.00843116641}}, + {"un1", []float64{78.75, 80.15625, 1.40625, 2.8125}}, + {"b6rv0shhd5", []float64{58.5579174757, 58.5579228401, -157.824010849, -157.82400012}}, + {"qyg8d", []float64{-6.943359375, -6.8994140625, 128.759765625, 128.803710938}}, + {"3p7f2mvx6", []float64{-3.79041194916, -3.79036903381, -129.707937241, -129.707894325}}, + {"u0vj", []float64{50.09765625, 50.2734375, 7.03125, 7.3828125}}, + {"49m", []float64{-82.96875, -81.5625, -60.46875, -59.0625}}, + {"7vhu8hp00j2", []float64{-16.0619835556, -16.0619822145, -4.56069946289, -4.56069812179}}, + {"3sh", []float64{-22.5, -21.09375, -106.875, -105.46875}}, + {"sexg40hc", []float64{20.2150154114, 20.2151870728, 33.4928512573, 33.4931945801}}, + {"4uwxw8u", []float64{-63.365020752, -63.3636474609, -46.8182373047, -46.8168640137}}, + {"mr", []float64{-5.625, 0.0, 56.25, 67.5}}, + {"1kymkgft79d3", []float64{-62.3368896358, -62.3368894681, -114.748610817, -114.748610482}}, + {"3tj", []float64{-16.875, -15.46875, -105.46875, -104.0625}}, + {"vxfzth5", []float64{89.9340820312, 89.9354553223, 71.5910339355, 71.5924072266}}, + {"h", []float64{-90.0, -45.0, 0.0, 45.0}}, + {"e5r9", []float64{18.45703125, 18.6328125, -34.453125, -34.1015625}}, + {"hq9bc8pmfk", []float64{-53.3046555519, -53.3046501875, 13.7869083881, 13.786919117}}, + {"05t", []float64{-70.3125, -68.90625, -172.96875, -171.5625}}, + {"ye6yg", []float64{64.4677734375, 64.51171875, 116.499023438, 116.54296875}}, + {"gg4k", []float64{62.578125, 62.75390625, -8.0859375, -7.734375}}, + {"50q2fcp", []float64{-88.4564208984, -88.4550476074, -36.0804748535, -36.0791015625}}, + {"2hu8d3131", []float64{-18.1876945496, -18.1876516342, -173.571238518, -173.571195602}}, + {"08gcdmy84ewg", []float64{-85.4859731533, -85.4859729856, -152.118642814, -152.118642479}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"t05v4wzmqmet", []float64{0.91691667214, 0.916916839778, 50.3935300559, 50.3935303912}}, + {"3nc", []float64{-7.03125, -5.625, -133.59375, -132.1875}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"3gvk6zm1869", []float64{-23.1190833449, -23.1190820038, -93.7394593656, -93.7394580245}}, + {"39rruq", []float64{-36.5734863281, -36.5679931641, -102.117919922, -102.106933594}}, + {"jhkxvqt7q5z", []float64{-64.6951617301, -64.6951603889, 51.5663145483, 51.5663158894}}, + {"bb", []float64{45.0, 50.625, -146.25, -135.0}}, + {"es26styfpb", []float64{24.3776321411, 24.3776375055, -21.9410812855, -21.9410705566}}, + {"500q98r9", []float64{-88.8558769226, -88.8557052612, -44.5722198486, -44.5718765259}}, + {"kv", []float64{-16.875, -11.25, 33.75, 45.0}}, + {"njf", []float64{-57.65625, -56.25, 92.8125, 94.21875}}, + {"whk5vu", []float64{24.5874023438, 24.5928955078, 95.8776855469, 95.888671875}}, + {"w9pq4yk4p4qf", []float64{6.71437550336, 6.714375671, 122.821964733, 122.821965069}}, + {"yt", []float64{73.125, 78.75, 112.5, 123.75}}, + {"ccztdek", []float64{55.8283996582, 55.8297729492, -90.5877685547, -90.5863952637}}, + {"ej33gfth2", []float64{29.8533296585, 29.8533725739, -43.070526123, -43.0704832077}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"hqyfjm", []float64{-51.6522216797, -51.6467285156, 20.9729003906, 20.9838867188}}, + {"njcr", []float64{-56.42578125, -56.25, 91.7578125, 92.109375}}, + {"48ejbxwrk6", []float64{-86.1343038082, -86.1342984438, -63.2505118847, -63.2505011559}}, + {"78f1r09e5v8", []float64{-40.558232367, -40.5582310259, -19.3776619434, -19.3776606023}}, + {"ged8fvuhk31", []float64{64.8516565561, 64.8516578972, -18.8578484952, -18.8578471541}}, + {"8ss3fn", []float64{25.6530761719, 25.6585693359, -151.435546875, -151.424560547}}, + {"v90sp4e6", []float64{51.3422012329, 51.3423728943, 68.5152053833, 68.5155487061}}, + {"bx00h848", []float64{84.375, 84.3751716614, -157.298812866, -157.298469543}}, + {"9y3", []float64{35.15625, 36.5625, -99.84375, -98.4375}}, + {"ehpkg7", []float64{23.3514404297, 23.3569335938, -34.6618652344, -34.6508789062}}, + {"r38623wxhs", []float64{-36.1575293541, -36.1575239897, 146.621668339, 146.621679068}}, + {"x6yex2zx", []float64{16.0893058777, 16.0894775391, 155.719528198, 155.719871521}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"6w", []float64{-11.25, -5.625, -67.5, -56.25}}, + {"mx6g2d", []float64{-3.63647460938, -3.63098144531, 71.3891601562, 71.4001464844}}, + {"vmsh9b6f", []float64{76.7302322388, 76.7304039001, 61.9556808472, 61.9560241699}}, + {"7uqbsm728bjw", []float64{-20.9769334272, -20.9769332595, -1.56654216349, -1.56654182822}}, + {"nvtqqh0jc3", []float64{-57.9409021139, -57.9408967495, 131.396538019, 131.396548748}}, + {"x8", []float64{0.0, 5.625, 157.5, 168.75}}, + {"nqx5n", []float64{-52.91015625, -52.8662109375, 111.357421875, 111.401367188}}, + {"c4wmv60xtud", []float64{60.0855401158, 60.0855414569, -125.979288518, -125.979287177}}, + {"t9", []float64{5.625, 11.25, 67.5, 78.75}}, + {"e3nqrsk9fqdu", []float64{6.74731470644, 6.74731487408, -24.6250675991, -24.6250672638}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"7x9", []float64{-2.8125, -1.40625, -21.09375, -19.6875}}, + {"760qb6yxf", []float64{-32.5470399857, -32.5469970703, -33.3784389496, -33.3783960342}}, + {"x7skftff", []float64{20.5543899536, 20.554561615, 152.340202332, 152.340545654}}, + {"jv774u", []float64{-59.9194335938, -59.9139404297, 83.4411621094, 83.4521484375}}, + {"phh7", []float64{-66.97265625, -66.796875, 140.9765625, 141.328125}}, + {"hv4tt0pymy", []float64{-60.9070980549, -60.9070926905, 37.4962413311, 37.4962520599}}, + {"3zemdehyubj", []float64{-1.82806491852, -1.82806357741, -96.563090533, -96.5630891919}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"dep80ett", []float64{16.8950843811, 16.8952560425, -56.9235992432, -56.9232559204}}, + {"bbj", []float64{45.0, 46.40625, -139.21875, -137.8125}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"9k1t0ecqh3v3", []float64{23.4005451389, 23.4005453065, -121.616746299, -121.616745964}}, + {"mm5", []float64{-16.875, -15.46875, 60.46875, 61.875}}, + {"jz4zmmv2", []float64{-49.3190002441, -49.3188285828, 82.8551101685, 82.8554534912}}, + {"bpshcry", []float64{88.065032959, 88.06640625, -174.311828613, -174.310455322}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"e87b", []float64{1.40625, 1.58203125, -17.2265625, -16.875}}, + {"pcphekd1ph0", []float64{-83.5590720177, -83.5590706766, 178.739619255, 178.739620596}}, + {"nrwq0", []float64{-46.7578125, -46.7138671875, 110.0390625, 110.083007812}}, + {"6k3e7rk823cj", []float64{-20.4825823568, -20.4825821891, -76.4916108549, -76.4916105196}}, + {"kkv059k5v", []float64{-18.2737398148, -18.2736968994, 18.4407663345, 18.4408092499}}, + {"ep7x2t4t6x", []float64{42.084068656, 42.0840740204, -40.0526118279, -40.052601099}}, + {"43hh0", []float64{-83.671875, -83.6279296875, -73.125, -73.0810546875}}, + {"rdhfdg0qee", []float64{-33.2929354906, -33.2929301262, 164.301030636, 164.301041365}}, + {"znku45j7p", []float64{80.8763694763, 80.8764123917, 141.77508831, 141.775131226}}, + {"ju", []float64{-67.5, -61.875, 78.75, 90.0}}, + {"b6zckuz", []float64{60.7145690918, 60.7159423828, -157.633209229, -157.631835938}}, + {"fm7m", []float64{75.41015625, 75.5859375, -74.1796875, -73.828125}}, + {"8xg", []float64{43.59375, 45.0, -153.28125, -151.875}}, + {"wfk7q", []float64{13.2275390625, 13.271484375, 129.990234375, 130.034179688}}, + {"6r1p9yz6z9", []float64{-4.26908433437, -4.26907896996, -77.2565674782, -77.2565567493}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"fuzw5", []float64{72.7734375, 72.8173828125, -45.5712890625, -45.52734375}}, + {"m33ehjv", []float64{-37.4098205566, -37.4084472656, 58.5420227051, 58.5433959961}}, + {"s6stp", []float64{14.94140625, 14.9853515625, 17.8857421875, 17.9296875}}, + {"tjxuvxh", []float64{31.8109130859, 31.812286377, 56.1456298828, 56.1470031738}}, + {"0vgxezccsf", []float64{-56.2950503826, -56.2950450182, -141.160722971, -141.160712242}}, + {"0h", []float64{-67.5, -61.875, -180.0, -168.75}}, + {"ge6bb", []float64{63.4130859375, 63.45703125, -18.6328125, -18.5888671875}}, + {"h2gyy9", []float64{-84.5892333984, -84.5837402344, 16.8090820312, 16.8200683594}}, + {"g0f7xg", []float64{49.8504638672, 49.8559570312, -41.4953613281, -41.484375}}, + {"ujfk5", []float64{78.046875, 78.0908203125, 3.2958984375, 3.33984375}}, + {"q0b6rwm0", []float64{-40.3514099121, -40.3512382507, 90.6880187988, 90.6883621216}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"0nyhwsx2g5", []float64{-51.2153702974, -51.215364933, -171.266770363, -171.266759634}}, + {"mjpe", []float64{-16.34765625, -16.171875, 55.546875, 55.8984375}}, + {"mt", []float64{-16.875, -11.25, 67.5, 78.75}}, + {"z49vj0d4zz", []float64{59.9446624517, 59.9446678162, 137.683743238, 137.683753967}}, + {"zry97vd3gs", []float64{88.8440108299, 88.8440161943, 155.55866003, 155.558670759}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"v3syrvc9", []float64{54.5678901672, 54.5680618286, 63.2723236084, 63.2726669312}}, + {"nrer", []float64{-46.58203125, -46.40625, 105.8203125, 106.171875}}, + {"2hqt8nf65v5e", []float64{-20.0895036198, -20.0895034522, -170.856119469, -170.856119134}}, + {"wekgdxc", []float64{18.9390563965, 18.9404296875, 119.290924072, 119.292297363}}, + {"6bh43q", []float64{-44.5715332031, -44.5660400391, -50.5700683594, -50.5590820312}}, + {"d564", []float64{18.6328125, 18.80859375, -87.1875, -86.8359375}}, + {"m85", []float64{-45.0, -43.59375, 71.71875, 73.125}}, + {"x4tj5rb77r", []float64{14.9845737219, 14.9845790863, 142.174555063, 142.174565792}}, + {"0bwycz8vr", []float64{-85.9588766098, -85.9588336945, -136.679577827, -136.679534912}}, + {"ehnu2e", []float64{23.2635498047, 23.2690429688, -35.4858398438, -35.4748535156}}, + {"jbe8mk", []float64{-87.1215820312, -87.1160888672, 83.9025878906, 83.9135742188}}, + {"ss", []float64{22.5, 28.125, 22.5, 33.75}}, + {"8edywpv3ygj3", []float64{20.8729668148, 20.8729669824, -153.361634128, -153.361633793}}, + {"xpxu1erq390", []float64{42.9095560312, 42.9095573723, 145.974376202, 145.974377543}}, + {"4043qb91kj", []float64{-89.7772854567, -89.7772800922, -86.5377616882, -86.5377509594}}, + {"2n7q0j0r", []float64{-8.76039505005, -8.76022338867, -175.429344177, -175.429000854}}, + {"cm0n96q3vn8t", []float64{74.2802738585, 74.2802740261, -123.686270043, -123.686269708}}, + {"50hgpxg", []float64{-89.4300842285, -89.4287109375, -37.9866027832, -37.9852294922}}, + {"2m", []float64{-16.875, -11.25, -168.75, -157.5}}, + {"w4s1zj8n9", []float64{14.4014453888, 14.4014883041, 95.9326601028, 95.9327030182}}, + {"hxzh", []float64{-45.703125, -45.52734375, 32.34375, 32.6953125}}, + {"cn", []float64{78.75, 84.375, -135.0, -123.75}}, + {"dpt5k8", []float64{42.7587890625, 42.7642822266, -82.7709960938, -82.7600097656}}, + {"gz", []float64{84.375, 90.0, -11.25, 0.0}}, + {"d09knt", []float64{3.54309082031, 3.54858398438, -87.9565429688, -87.9455566406}}, + {"mrucw", []float64{-1.142578125, -1.0986328125, 63.193359375, 63.2373046875}}, + {"055egqf4y", []float64{-72.4282693863, -72.4282264709, -174.93229866, -174.932255745}}, + {"qphu", []float64{-4.921875, -4.74609375, 96.6796875, 97.03125}}, + {"dfeh", []float64{14.765625, 14.94140625, -52.03125, -51.6796875}}, + {"00qfn4ctp57", []float64{-88.2262055576, -88.2262042165, -170.241776258, -170.241774917}}, + {"q9xx27p2trn", []float64{-35.2714830637, -35.2714817226, 123.06805104, 123.068052381}}, + {"10bhfgfsj8m", []float64{-84.9250017107, -84.9250003695, -134.875474423, -134.875473082}}, + {"6k", []float64{-22.5, -16.875, -78.75, -67.5}}, + {"zyvjr4m37", []float64{83.9041757584, 83.9042186737, 176.096205711, 176.096248627}}, + {"0c8k7gb", []float64{-80.7948303223, -80.7934570312, -145.733642578, -145.732269287}}, + {"n7k", []float64{-71.71875, -70.3125, 106.875, 108.28125}}, + {"fpj4eecz", []float64{84.8362541199, 84.8364257812, -82.812538147, -82.8121948242}}, + {"ev", []float64{28.125, 33.75, -11.25, 0.0}}, + {"8y", []float64{33.75, 39.375, -146.25, -135.0}}, + {"x9xd1q", []float64{8.82202148438, 8.82751464844, 168.101806641, 168.112792969}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"d1pmefm0t", []float64{6.60424232483, 6.60428524017, -79.6328115463, -79.632768631}}, + {"tmy", []float64{32.34375, 33.75, 64.6875, 66.09375}}, + {"phu6jzv0z", []float64{-62.8869867325, -62.8869438171, 141.236414909, 141.236457825}}, + {"zv7pjxpuwjbq", []float64{75.8009752259, 75.8009753935, 173.221350051, 173.221350387}}, + {"9gegu1", []float64{20.3521728516, 20.3576660156, -95.80078125, -95.7897949219}}, + {"tq33", []float64{35.33203125, 35.5078125, 58.0078125, 58.359375}}, + {"e", []float64{0.0, 45.0, -45.0, 0.0}}, + {"y6z1sy", []float64{60.7653808594, 60.7708740234, 111.302490234, 111.313476562}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"q", []float64{-45.0, 0.0, 90.0, 135.0}}, + {"mtyyw7", []float64{-11.4971923828, -11.4916992188, 77.2668457031, 77.2778320312}}, + {"2sqmb", []float64{-20.0830078125, -20.0390625, -148.7109375, -148.666992188}}, + {"yp487k", []float64{84.4409179688, 84.4464111328, 93.6584472656, 93.6694335938}}, + {"nyn5k1fc", []float64{-55.668926239, -55.6687545776, 132.3670578, 132.367401123}}, + {"gnqk77v", []float64{80.9239196777, 80.9252929688, -36.0612487793, -36.0598754883}}, + {"yw12", []float64{78.75, 78.92578125, 114.2578125, 114.609375}}, + {"vcgqqmd", []float64{55.9725952148, 55.9739685059, 83.5977172852, 83.5990905762}}, + {"27zp3q", []float64{-22.5988769531, -22.5933837891, -158.851318359, -158.840332031}}, + {"cg4tb6", []float64{62.8967285156, 62.9022216797, -97.7233886719, -97.7124023438}}, + {"w5njb9rnp5e", []float64{17.8936573863, 17.8936587274, 98.4693901241, 98.4693914652}}, + {"9y6rp4pptv", []float64{36.3990193605, 36.399024725, -97.7684605122, -97.7684497833}}, + {"pjup17j08hup", []float64{-56.4091892727, -56.409189105, 140.68680346, 140.686803795}}, + {"vz52f33z9mu", []float64{84.5150206983, 84.5150220394, 83.421651721, 83.4216530621}}, + {"zd", []float64{56.25, 61.875, 157.5, 168.75}}, + {"q", []float64{-45.0, 0.0, 90.0, 135.0}}, + {"dm46s", []float64{28.564453125, 28.6083984375, -75.41015625, -75.3662109375}}, + {"d7dnvbu", []float64{20.8781433105, 20.8795166016, -75.6793212891, -75.677947998}}, + {"02ercsvx0h", []float64{-85.7978796959, -85.7978743315, -164.106216431, -164.106205702}}, + {"hnfx5f", []float64{-50.7897949219, -50.7843017578, 3.68041992188, 3.69140625}}, + {"evb677f6t", []float64{32.7602863312, 32.7603292465, -10.7523107529, -10.7522678375}}, + {"sg43r7p151", []float64{17.1113830805, 17.1113884449, 37.2424077988, 37.2424185276}}, + {"441mdz5", []float64{-77.7447509766, -77.7433776855, -88.1172180176, -88.1158447266}}, + {"k2qrsc7hr81", []float64{-42.2677946091, -42.267793268, 20.2522458136, 20.2522471547}}, + {"d0ydzxhjwr", []float64{4.74158227444, 4.74158763885, -80.5240237713, -80.5240130424}}, + {"4cn", []float64{-84.375, -82.96875, -47.8125, -46.40625}}, + {"ds2", []float64{23.90625, 25.3125, -67.5, -66.09375}}, + {"rxvywy", []float64{-0.230712890625, -0.225219726562, 165.882568359, 165.893554688}}, + {"zs2k", []float64{69.609375, 69.78515625, 157.8515625, 158.203125}}, + {"kg63", []float64{-26.54296875, -26.3671875, 36.9140625, 37.265625}}, + {"hmxh64j", []float64{-58.3044433594, -58.3030700684, 21.1885070801, 21.1898803711}}, + {"d5v3", []float64{21.26953125, 21.4453125, -82.6171875, -82.265625}}, + {"ddg1", []float64{15.64453125, 15.8203125, -63.28125, -62.9296875}}, + {"stf4tug", []float64{32.8092956543, 32.8106689453, 25.5693054199, 25.5706787109}}, + {"vgc", []float64{66.09375, 67.5, 80.15625, 81.5625}}, + {"jby3xf", []float64{-85.5065917969, -85.5010986328, 87.8796386719, 87.890625}}, + {"9b1f419tdjf", []float64{0.360777229071, 0.360778570175, -98.6990234256, -98.6990220845}}, + {"0zqp4", []float64{-47.98828125, -47.9443359375, -137.724609375, -137.680664062}}, + {"gg292c4wd", []float64{63.5075855255, 63.5076284409, -10.5103969574, -10.5103540421}}, + {"zy96qbt", []float64{81.9607543945, 81.9621276855, 170.811309814, 170.812683105}}, + {"tz9x7f5c70", []float64{43.4731149673, 43.4731203318, 81.0294485092, 81.0294592381}}, + {"rq", []float64{-11.25, -5.625, 146.25, 157.5}}, + {"94tt", []float64{14.94140625, 15.1171875, -127.265625, -126.9140625}}, + {"h7vnwf8", []float64{-67.7499389648, -67.7485656738, 18.5778808594, 18.5792541504}}, + {"4f", []float64{-78.75, -73.125, -56.25, -45.0}}, + {"kj3t", []float64{-14.58984375, -14.4140625, 2.109375, 2.4609375}}, + {"qspj", []float64{-21.62109375, -21.4453125, 122.34375, 122.6953125}}, + {"4y9", []float64{-53.4375, -52.03125, -54.84375, -53.4375}}, + {"b05kqcvsm8", []float64{45.7574129105, 45.7574182749, -175.125267506, -175.125256777}}, + {"p5zq4f", []float64{-67.8405761719, -67.8350830078, 145.316162109, 145.327148438}}, + {"1cgx", []float64{-78.92578125, -78.75, -96.328125, -95.9765625}}, + {"m2", []float64{-45.0, -39.375, 56.25, 67.5}}, + {"j150xkd492", []float64{-84.2619609833, -84.2619556189, 49.5401537418, 49.5401644707}}, + {"05rs", []float64{-71.015625, -70.83984375, -169.453125, -169.1015625}}, + {"ve8", []float64{64.6875, 66.09375, 67.5, 68.90625}}, + {"r9tv", []float64{-35.68359375, -35.5078125, 165.5859375, 165.9375}}, + {"d71r07", []float64{18.1219482422, 18.1274414062, -76.9812011719, -76.9702148438}}, + {"b6hepfqk6", []float64{56.79043293, 56.7904758453, -162.072629929, -162.072587013}}, + {"md8y86mqjd", []float64{-29.7815215588, -29.7815161943, 68.5731196404, 68.5731303692}}, + {"bcgcyq", []float64{55.1843261719, 55.1898193359, -140.701904297, -140.690917969}}, + {"e3hpu9352", []float64{6.99472904205, 6.9947719574, -27.9258728027, -27.9258298874}}, + {"hsbsq2rkmg", []float64{-62.5320607424, -62.532055378, 23.4879863262, 23.4879970551}}, + {"ub2r", []float64{47.63671875, 47.8125, 34.1015625, 34.453125}}, + {"d8", []float64{0.0, 5.625, -67.5, -56.25}}, + {"gexm9j6y088", []float64{65.6841686368, 65.6841699779, -12.2569441795, -12.2569428384}}, + {"15cdq", []float64{-68.5107421875, -68.466796875, -132.626953125, -132.583007812}}, + {"9zud17gy", []float64{43.9669418335, 43.9671134949, -94.8617935181, -94.8614501953}}, + {"4q3y", []float64{-53.7890625, -53.61328125, -76.2890625, -75.9375}}, + {"gph138", []float64{84.5947265625, 84.6002197266, -39.3090820312, -39.2980957031}}, + {"m09d8ju2b", []float64{-41.7163324356, -41.7162895203, 47.1152114868, 47.1152544022}}, + {"8mszup4gk", []float64{32.3388147354, 32.3388576508, -161.890583038, -161.890540123}}, + {"dyrfvtvqh", []float64{35.6722640991, 35.6723070145, -45.102481842, -45.1024389267}}, + {"3h9tgkp", []float64{-18.6547851562, -18.6534118652, -132.738189697, -132.736816406}}, + {"66gdty14u", []float64{-29.0583658218, -29.0583229065, -73.5738945007, -73.5738515854}}, + {"83zp1d", []float64{11.0852050781, 11.0906982422, -158.840332031, -158.829345703}}, + {"e7gp0", []float64{22.32421875, 22.3681640625, -29.53125, -29.4873046875}}, + {"s0ykfgceytk", []float64{5.07498219609, 5.0749835372, 8.91225636005, 8.91225770116}}, + {"zfe7", []float64{59.58984375, 59.765625, 173.3203125, 173.671875}}, + {"cr9", []float64{87.1875, 88.59375, -122.34375, -120.9375}}, + {"9ugr3kq", []float64{28.0165100098, 28.0178833008, -96.6165161133, -96.6151428223}}, + {"2grcq0", []float64{-26.4990234375, -26.4935302734, -135.087890625, -135.076904297}}, + {"50bb31vkds3p", []float64{-85.726895202, -85.7268950343, -43.8940487802, -43.8940484449}}, + {"qhxdv1hcqe3", []float64{-19.1983763874, -19.1983750463, 100.773404986, 100.773406327}}, + {"k2cv0gp2vk", []float64{-39.8857140541, -39.8857086897, 13.7540781498, 13.7540888786}}, + {"y5jd", []float64{62.2265625, 62.40234375, 97.734375, 98.0859375}}, + {"gnvg3p", []float64{83.5784912109, 83.583984375, -36.8701171875, -36.8591308594}}, + {"9g70w", []float64{18.369140625, 18.4130859375, -96.767578125, -96.7236328125}}, + {"9g7qsb", []float64{19.423828125, 19.4293212891, -96.4709472656, -96.4599609375}}, + {"1zq2u", []float64{-49.0869140625, -49.04296875, -92.28515625, -92.2412109375}}, + {"tr", []float64{39.375, 45.0, 56.25, 67.5}}, + {"4wmddz9mgk38", []float64{-54.3620882928, -54.3620881252, -59.6429172903, -59.6429169551}}, + {"wkcdsn8nbz", []float64{27.1951049566, 27.195110321, 103.535188437, 103.535199165}}, + {"1r198wxj", []float64{-50.3247642517, -50.3245925903, -121.609039307, -121.608695984}}, + {"eu", []float64{22.5, 28.125, -11.25, 0.0}}, + {"k2y7mk6sd0wz", []float64{-40.1858386584, -40.1858384907, 20.2733035013, 20.2733038366}}, + {"gms9ytw4v5", []float64{76.2758177519, 76.2758231163, -27.1277761459, -27.1277654171}}, + {"2vdkc", []float64{-13.2275390625, -13.18359375, -143.041992188, -142.998046875}}, + {"bke7fx3", []float64{71.011505127, 71.012878418, -164.068450928, -164.067077637}}, + {"tvnxu7jt8", []float64{29.5047283173, 29.5047712326, 88.0849456787, 88.0849885941}}, + {"f864yp3c", []float64{46.9296455383, 46.9298171997, -64.4214248657, -64.421081543}}, + {"g8hxr7x150d7", []float64{46.2938149832, 46.2938151509, -15.8435266837, -15.8435263485}}, + {"zmk4kmh", []float64{74.9542236328, 74.9555969238, 152.067260742, 152.068634033}}, + {"gtqsvep4", []float64{75.3830337524, 75.3832054138, -13.1080627441, -13.1077194214}}, + {"trsvy0", []float64{43.1982421875, 43.2037353516, 63.193359375, 63.2043457031}}, + {"bevjfs", []float64{67.1264648438, 67.1319580078, -150.358886719, -150.347900391}}, + {"ktrb", []float64{-15.46875, -15.29296875, 33.3984375, 33.75}}, + {"dn20q1pv", []float64{35.2065467834, 35.2067184448, -89.7256851196, -89.7253417969}}, + {"8n3wy5g2", []float64{36.3633728027, 36.3635444641, -177.622489929, -177.622146606}}, + {"vyzft", []float64{83.408203125, 83.4521484375, 89.8681640625, 89.912109375}}, + {"gwuedjbs8222", []float64{83.6163438857, 83.6163440533, -16.0832866654, -16.0832863301}}, + {"fpb89dkktj83", []float64{88.6948023923, 88.6948025599, -89.2249056324, -89.2249052972}}, + {"wjjk3", []float64{28.8720703125, 28.916015625, 97.4267578125, 97.470703125}}, + {"wx6", []float64{40.78125, 42.1875, 115.3125, 116.71875}}, + {"yzpuuv3w0pw", []float64{85.2398702502, 85.2398715913, 134.859245718, 134.859247059}}, + {"k2518ucy", []float64{-44.7092056274, -44.7090339661, 15.5041122437, 15.5044555664}}, + {"hkjk1075", []float64{-66.7949867249, -66.7948150635, 18.6808776855, 18.6812210083}}, + {"btmd", []float64{74.8828125, 75.05859375, -149.765625, -149.4140625}}, + {"ucbvmyuw9z", []float64{55.8048337698, 55.8048391342, 35.0636279583, 35.0636386871}}, + {"wf86ytd34", []float64{14.5762825012, 14.5763254166, 124.390382767, 124.390425682}}, + {"9zjbws3u", []float64{39.4869232178, 39.4870948792, -92.8760147095, -92.8756713867}}, + {"rqc", []float64{-7.03125, -5.625, 147.65625, 149.0625}}, + {"pwqw8gh8x74", []float64{-53.6845904589, -53.6845891178, 166.680077612, 166.680078954}}, + {"5ekn", []float64{-70.6640625, -70.48828125, -16.875, -16.5234375}}, + {"mxx0b3mzwxp", []float64{-2.67247259617, -2.67247125506, 77.3629210889, 77.36292243}}, + {"tn", []float64{33.75, 39.375, 45.0, 56.25}}, + {"ju59u9b8grr", []float64{-67.1826021373, -67.1826007962, 83.8704644144, 83.8704657555}}, + {"hmte6v6jzq84", []float64{-58.4613495693, -58.4613494016, 19.1082823277, 19.1082826629}}, + {"t3", []float64{5.625, 11.25, 56.25, 67.5}}, + {"es9g6", []float64{25.8837890625, 25.927734375, -19.951171875, -19.9072265625}}, + {"2bwcdk6y", []float64{-41.8994522095, -41.8992805481, -136.655158997, -136.654815674}}, + {"4ew3jn8umh", []float64{-70.1002621651, -70.1002568007, -58.4899663925, -58.4899556637}}, + {"0meekufm4x3", []float64{-58.4642212093, -58.4642198682, -163.616186231, -163.61618489}}, + {"02", []float64{-90.0, -84.375, -168.75, -157.5}}, + {"9yuv0t43sv0", []float64{38.8754063845, 38.8754077256, -94.5450460911, -94.54504475}}, + {"u0g7y8444", []float64{49.8782730103, 49.8783159256, 4.85878944397, 4.85883235931}}, + {"r4", []float64{-33.75, -28.125, 135.0, 146.25}}, + {"1ps631dwde", []float64{-47.4076205492, -47.4076151848, -128.975951672, -128.975940943}}, + {"vbmfdm", []float64{46.8731689453, 46.8786621094, 86.9348144531, 86.9458007812}}, + {"s7gmxm8vhn5v", []float64{22.0916506089, 22.0916507766, 16.1401226744, 16.1401230097}}, + {"mcwrh68t", []float64{-35.317440033, -35.3172683716, 87.7265167236, 87.7268600464}}, + {"9yt5645jq9r9", []float64{37.145683486, 37.1456836537, -94.1264504939, -94.1264501587}}, + {"61t1x3z", []float64{-36.2892150879, -36.2878417969, -82.6405334473, -82.6391601562}}, + {"wmzkmbm", []float64{33.0921936035, 33.0935668945, 111.704864502, 111.706237793}}, + {"jy", []float64{-56.25, -50.625, 78.75, 90.0}}, + {"9ctckntmvgkr", []float64{8.69393778965, 8.69393795729, -92.9808190092, -92.980818674}}, + {"8rnugbss4gd", []float64{40.2134129405, 40.2134142816, -159.086717069, -159.086715728}}, + {"ek8", []float64{25.3125, 26.71875, -33.75, -32.34375}}, + {"nvqegbu", []float64{-59.8054504395, -59.8040771484, 133.060913086, 133.062286377}}, + {"h0u7v6pujp5", []float64{-85.1103597879, -85.1103584468, 6.21813699603, 6.21813833714}}, + {"2rj", []float64{-5.625, -4.21875, -161.71875, -160.3125}}, + {"jk38", []float64{-66.09375, -65.91796875, 58.359375, 58.7109375}}, + {"c67qe30m", []float64{58.8051795959, 58.8053512573, -119.036521912, -119.036178589}}, + {"xhtwk", []float64{26.4111328125, 26.455078125, 142.91015625, 142.954101562}}, + {"pw6ue97rjn11", []float64{-54.0446339361, -54.0446337685, 161.525675207, 161.525675543}}, + {"xpmw424pk6", []float64{41.8371927738, 41.8371981382, 142.836180925, 142.836191654}}, + {"rk", []float64{-22.5, -16.875, 146.25, 157.5}}, + {"hmfmn5uwdh2", []float64{-56.755605787, -56.7556044459, 14.6840000153, 14.6840013564}}, + {"defvfj", []float64{22.1319580078, 22.1374511719, -63.544921875, -63.5339355469}}, + {"sg7jd89w", []float64{19.2518234253, 19.2519950867, 38.0806732178, 38.0810165405}}, + {"yn0", []float64{78.75, 80.15625, 90.0, 91.40625}}, + {"3n7re6gv2e92", []float64{-8.50936442614, -8.5093642585, -130.281692259, -130.281691924}}, + {"vyj", []float64{78.75, 80.15625, 85.78125, 87.1875}}, + {"9cntsz", []float64{6.63024902344, 6.6357421875, -91.9006347656, -91.8896484375}}, + {"w4uwq3d", []float64{16.5756225586, 16.5769958496, 96.6055297852, 96.6069030762}}, + {"zffrf", []float64{61.8310546875, 61.875, 172.001953125, 172.045898438}}, + {"hq6142s", []float64{-54.665222168, -54.663848877, 14.1668701172, 14.1682434082}}, + {"m7srp977", []float64{-24.0746498108, -24.0744781494, 62.5606155396, 62.5609588623}}, + {"8v6hxy3sz", []float64{30.3574132919, 30.3574562073, -143.094563484, -143.094520569}}, + {"rf4snrh70h", []float64{-33.0078864098, -33.0078810453, 172.54611969, 172.546130419}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"ssr6y", []float64{24.3896484375, 24.43359375, 32.958984375, 33.0029296875}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"yz", []float64{84.375, 90.0, 123.75, 135.0}}, + {"n2", []float64{-90.0, -84.375, 101.25, 112.5}}, + {"vfe", []float64{59.0625, 60.46875, 82.96875, 84.375}}, + {"h", []float64{-90.0, -45.0, 0.0, 45.0}}, + {"z3", []float64{50.625, 56.25, 146.25, 157.5}}, + {"z2web5usz", []float64{48.4930944443, 48.4931373596, 155.397105217, 155.397148132}}, + {"8gkc", []float64{18.45703125, 18.6328125, -139.5703125, -139.21875}}, + {"17de4", []float64{-69.78515625, -69.7412109375, -120.146484375, -120.102539062}}, + {"ky71b3fwd2vv", []float64{-9.52539911494, -9.5253989473, 37.9832738265, 37.9832741618}}, + {"e4gfcm", []float64{15.9796142578, 15.9851074219, -39.6716308594, -39.6606445312}}, + {"kdgwxnnb", []float64{-28.3557128906, -28.3555412292, 27.7387619019, 27.7391052246}}, + {"4nyn2chy", []float64{-50.9260940552, -50.9259223938, -81.5230178833, -81.5226745605}}, + {"u6je3kk57", []float64{56.8451929092, 56.8452358246, 19.0449285507, 19.0449714661}}, + {"nrs9j1hj5tj", []float64{-47.630340457, -47.6303391159, 107.803501636, 107.803502977}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"krqrcc2hm9uq", []float64{-2.84883890301, -2.84883873537, 20.116208531, 20.1162088662}}, + {"fmceu", []float64{78.0029296875, 78.046875, -76.46484375, -76.4208984375}}, + {"q3w576", []float64{-35.9802246094, -35.9747314453, 109.830322266, 109.841308594}}, + {"19ehn", []float64{-80.859375, -80.8154296875, -108.017578125, -107.973632812}}, + {"zpkvjk", []float64{86.6821289062, 86.6876220703, 141.910400391, 141.921386719}}, + {"7cgwy9jm", []float64{-33.9633750916, -33.9632034302, -6.03527069092, -6.03492736816}}, + {"jju1cefk5v1", []float64{-57.3273199797, -57.3273186386, 50.6941701472, 50.6941714883}}, + {"5tfpq6", []float64{-56.3708496094, -56.3653564453, -19.4128417969, -19.4018554688}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"dm0vh8", []float64{29.00390625, 29.0093994141, -77.4975585938, -77.4865722656}}, + {"jt5c8hhd58b", []float64{-61.5890081227, -61.5890067816, 72.7797675133, 72.7797688544}}, + {"4ttr", []float64{-57.83203125, -57.65625, -60.1171875, -59.765625}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"sp2d3v6wz", []float64{41.2067556381, 41.2067985535, 0.783762931824, 0.783805847168}}, + {"up1yhbspqy", []float64{85.4337108135, 85.4337161779, 2.67546057701, 2.67547130585}}, + {"1z4bx8dp1ex1", []float64{-50.5331422202, -50.5331420526, -97.0504023135, -97.0504019782}}, + {"76qbnz5n1937", []float64{-32.3042606749, -32.3042605072, -23.9569957182, -23.9569953829}}, + {"0w1bvcjb40xd", []float64{-56.112667881, -56.1126677133, -154.778384641, -154.778384306}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"yqbjwyvsmc", []float64{83.9733606577, 83.9733660221, 101.554430723, 101.554441452}}, + {"0hdp2tdy5", []float64{-63.3818435669, -63.3818006516, -177.161622047, -177.161579132}}, + {"s8yv34v", []float64{5.15670776367, 5.15808105469, 32.0429992676, 32.0443725586}}, + {"uc60k7w6", []float64{52.0947647095, 52.0949363708, 36.757850647, 36.7581939697}}, + {"en6h7me8", []float64{35.9335327148, 35.9337043762, -42.0398712158, -42.0395278931}}, + {"1bks34", []float64{-87.8356933594, -87.8302001953, -94.8779296875, -94.8669433594}}, + {"65", []float64{-28.125, -22.5, -90.0, -78.75}}, + {"qwrquvr", []float64{-8.62838745117, -8.62701416016, 122.913665771, 122.915039062}}, + {"3dmchcusw83", []float64{-32.1575818956, -32.1575805545, -104.198862165, -104.198860824}}, + {"urfeh", []float64{89.12109375, 89.1650390625, 14.94140625, 14.9853515625}}, + {"d3g6rs", []float64{10.2612304688, 10.2667236328, -73.8500976562, -73.8391113281}}, + {"wx0v", []float64{40.25390625, 40.4296875, 113.5546875, 113.90625}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"et", []float64{28.125, 33.75, -22.5, -11.25}}, + {"dqtd0", []float64{36.9140625, 36.9580078125, -71.015625, -70.9716796875}}, + {"vtzuwhxhgjv8", []float64{78.1603311002, 78.1603312679, 78.6718585342, 78.6718588695}}, + {"bn9", []float64{81.5625, 82.96875, -178.59375, -177.1875}}, + {"685n", []float64{-43.9453125, -43.76953125, -63.28125, -62.9296875}}, + {"fqy4nhy", []float64{83.3464050293, 83.3477783203, -70.0405883789, -70.0392150879}}, + {"5q1dw", []float64{-55.810546875, -55.7666015625, -31.376953125, -31.3330078125}}, + {"0sv8m11dgkf", []float64{-63.2313139737, -63.2313126326, -149.543696344, -149.543695003}}, + {"vp435nn", []float64{84.5837402344, 84.5851135254, 48.3041381836, 48.3055114746}}, + {"wydj", []float64{37.44140625, 37.6171875, 126.5625, 126.9140625}}, + {"ebg8", []float64{4.21875, 4.39453125, -6.328125, -5.9765625}}, + {"ksmb6pfxe4", []float64{-21.0059344769, -21.0059291124, 30.6773900986, 30.6774008274}}, + {"8bv63qv96dp", []float64{4.65156197548, 4.65156331658, -138.804586083, -138.804584742}}, + {"0s0d53hd", []float64{-67.1426010132, -67.1424293518, -156.647872925, -156.647529602}}, + {"1yjf34ubpm", []float64{-55.8393591642, -55.8393537998, -93.1132829189, -93.1132721901}}, + {"823y79hmfe", []float64{2.51137912273, 2.51138448715, -166.129310131, -166.129299402}}, + {"xynnrv", []float64{34.8760986328, 34.8815917969, 177.528076172, 177.5390625}}, + {"9b9ejqcqqdu", []float64{3.37801024318, 3.37801158428, -98.9079111814, -98.9079098403}}, + {"cuuhfkd", []float64{72.5784301758, 72.5798034668, -95.5233764648, -95.5220031738}}, + {"khwceh", []float64{-19.4018554688, -19.3963623047, 9.6240234375, 9.63500976562}}, + {"z8vub32sy6", []float64{50.061403513, 50.0614088774, 165.597878695, 165.597889423}}, + {"4s", []float64{-67.5, -61.875, -67.5, -56.25}}, + {"bsrb4qeqt7eq", []float64{68.9430911466, 68.9430913143, -146.497992687, -146.497992352}}, + {"b1x0sc", []float64{53.5308837891, 53.5363769531, -169.947509766, -169.936523438}}, + {"1ngn2bn2vub", []float64{-50.9324629605, -50.9324616194, -130.739461184, -130.739459842}}, + {"bsm", []float64{68.90625, 70.3125, -150.46875, -149.0625}}, + {"xyzd7", []float64{38.3642578125, 38.408203125, 179.428710938, 179.47265625}}, + {"cvvf1tqs", []float64{77.7248382568, 77.7250099182, -93.0892181396, -93.0888748169}}, + {"fz2tpb1xj", []float64{86.6613578796, 86.661400795, -55.2040243149, -55.2039813995}}, + {"r4zqr7", []float64{-28.4161376953, -28.4106445312, 145.513916016, 145.524902344}}, + {"wth", []float64{28.125, 29.53125, 118.125, 119.53125}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"4j0ff5xs58h", []float64{-61.3716888428, -61.3716875017, -88.8469666243, -88.8469652832}}, + {"c15", []float64{50.625, 52.03125, -130.78125, -129.375}}, + {"tnkgyg", []float64{35.8319091797, 35.8374023438, 51.9763183594, 51.9873046875}}, + {"e99", []float64{8.4375, 9.84375, -21.09375, -19.6875}}, + {"bcrz69", []float64{53.3111572266, 53.3166503906, -135.241699219, -135.230712891}}, + {"e5w29f6", []float64{19.7877502441, 19.7891235352, -36.1312866211, -36.1299133301}}, + {"gykjjcq656b5", []float64{81.0423812829, 81.0423814505, -5.36359190941, -5.36359157413}}, + {"7j62nzypd3be", []float64{-15.4248806275, -15.4248804599, -41.5309696645, -41.5309693292}}, + {"rrsgbbvwt9r7", []float64{-2.14807743207, -2.14807726443, 152.970445342, 152.970445678}}, + {"93kub", []float64{7.8662109375, 7.91015625, -117.0703125, -117.026367188}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"02bcjtw5f84", []float64{-85.5746126175, -85.5746112764, -167.445263565, -167.445262223}}, + {"262dqsqs1", []float64{-31.9242095947, -31.9241666794, -167.752261162, -167.752218246}}, + {"185ss806c", []float64{-89.2085123062, -89.2084693909, -107.379984856, -107.37994194}}, + {"9s6", []float64{23.90625, 25.3125, -109.6875, -108.28125}}, + {"ych1d25e", []float64{50.8891868591, 50.8893585205, 129.478683472, 129.479026794}}, + {"7f2qcykht0d", []float64{-31.1221191287, -31.1221177876, -10.8158227801, -10.815821439}}, + {"5gk7qw", []float64{-71.1145019531, -71.1090087891, -4.98779296875, -4.97680664062}}, + {"7kjutjpu98", []float64{-21.6807460785, -21.6807407141, -25.4336285591, -25.4336178303}}, + {"h64", []float64{-78.75, -77.34375, 14.0625, 15.46875}}, + {"uy57", []float64{79.27734375, 79.453125, 38.3203125, 38.671875}}, + {"r5dtqkydk796", []float64{-24.3631505594, -24.3631503917, 138.799393661, 138.799393997}}, + {"5j", []float64{-61.875, -56.25, -45.0, -33.75}}, + {"xzbszzeu", []float64{44.4705963135, 44.4707679749, 169.798851013, 169.799194336}}, + {"wqjz0fj5e", []float64{34.9920558929, 34.9920988083, 109.375891685, 109.375934601}}, + {"dekz", []float64{19.51171875, 19.6875, -60.8203125, -60.46875}}, + {"bbyux", []float64{50.009765625, 50.0537109375, -136.450195312, -136.40625}}, + {"rctysz36", []float64{-35.3797531128, -35.3795814514, 177.046394348, 177.046737671}}, + {"xmhvqm1wcw7", []float64{29.0765096247, 29.0765109658, 153.206474036, 153.206475377}}, + {"nhw6c", []float64{-64.2041015625, -64.16015625, 98.8330078125, 98.876953125}}, + {"u9d3gdu", []float64{53.7602233887, 53.7615966797, 25.8233642578, 25.8247375488}}, + {"xenu1tyd0q", []float64{17.6100862026, 17.610091567, 167.067042589, 167.067053318}}, + {"qm70t", []float64{-15.380859375, -15.3369140625, 105.688476562, 105.732421875}}, + {"3g0", []float64{-28.125, -26.71875, -101.25, -99.84375}}, + {"fg", []float64{61.875, 67.5, -56.25, -45.0}}, + {"jq1tn6nn0hfs", []float64{-55.3590513021, -55.3590511344, 58.642276302, 58.6422766373}}, + {"b9kmw", []float64{52.998046875, 53.0419921875, -151.259765625, -151.215820312}}, + {"z7f9pj6", []float64{66.2983703613, 66.2997436523, 150.07598877, 150.077362061}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"hqbvrz4x", []float64{-51.0687446594, -51.068572998, 12.6486968994, 12.6490402222}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"81es", []float64{9.140625, 9.31640625, -175.078125, -174.7265625}}, + {"nyn6mf2", []float64{-55.8421325684, -55.8407592773, 132.791748047, 132.793121338}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"46hfqnuv", []float64{-78.3165550232, -78.3163833618, -71.8001174927, -71.7997741699}}, + {"7tne", []float64{-16.34765625, -16.171875, -13.359375, -13.0078125}}, + {"z7pkduh4g2c", []float64{62.6884643734, 62.6884657145, 156.571796089, 156.571797431}}, + {"xgmm", []float64{19.16015625, 19.3359375, 176.1328125, 176.484375}}, + {"he054x", []float64{-72.5592041016, -72.5537109375, 22.6098632812, 22.6208496094}}, + {"rqnu21r8g", []float64{-10.4959344864, -10.495891571, 155.752615929, 155.752658844}}, + {"k8xg8n36", []float64{-41.5375900269, -41.5374183655, 33.4001541138, 33.4004974365}}, + {"d60md", []float64{12.216796875, 12.2607421875, -78.310546875, -78.2666015625}}, + {"50tqctv", []float64{-85.9693908691, -85.9680175781, -37.5444030762, -37.5430297852}}, + {"yxknv54eqnz", []float64{86.984847039, 86.9848483801, 118.34842667, 118.348428011}}, + {"5zpczbcjgj", []float64{-50.3122490644, -50.3122437, -0.00948429107666, -0.0094735622406}}, + {"yp7nz0rr7d8", []float64{86.9704046845, 86.9704060256, 94.5364737511, 94.5364750922}}, + {"kfjb9s772f", []float64{-33.6381947994, -33.638189435, 41.9063508511, 41.9063615799}}, + {"q", []float64{-45.0, 0.0, 90.0, 135.0}}, + {"cx1f", []float64{84.7265625, 84.90234375, -110.0390625, -109.6875}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"hdtcdg62524", []float64{-75.6559753418, -75.6559740007, 30.7100191712, 30.7100205123}}, + {"fpy32", []float64{88.8134765625, 88.857421875, -81.2109375, -81.1669921875}}, + {"256qs", []float64{-25.576171875, -25.5322265625, -176.66015625, -176.616210938}}, + {"6rgzd89v3r48", []float64{-0.0842052698135, -0.0842051021755, -73.3642389625, -73.3642386273}}, + {"c8430hd23", []float64{45.2005434036, 45.200586319, -109.33280468, -109.332761765}}, + {"xtn", []float64{28.125, 29.53125, 165.9375, 167.34375}}, + {"1u2s9", []float64{-65.302734375, -65.2587890625, -100.502929688, -100.458984375}}, + {"5c80k35m35p", []float64{-81.512144208, -81.5121428668, -11.058716923, -11.0587155819}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"u7c0rfsdjn", []float64{66.1518037319, 66.1518090963, 13.0032205582, 13.003231287}}, + {"f4ce8gy", []float64{61.1045837402, 61.1059570312, -87.8494262695, -87.8480529785}}, + {"pbd4y02", []float64{-86.7027282715, -86.7013549805, 171.826171875, 171.827545166}}, + {"9n", []float64{33.75, 39.375, -135.0, -123.75}}, + {"ztybk5c8mw", []float64{77.4083697796, 77.408375144, 167.170264721, 167.17027545}}, + {"ks8pv0cv5u6j", []float64{-18.3201934956, -18.320193328, 22.7222934365, 22.7222937718}}, + {"nuh", []float64{-67.5, -66.09375, 129.375, 130.78125}}, + {"6khcf5xumxz", []float64{-22.1723856032, -22.1723842621, -71.9715334475, -71.9715321064}}, + {"nj2", []float64{-60.46875, -59.0625, 90.0, 91.40625}}, + {"mdd459nebt64", []float64{-30.5797721073, -30.5797719397, 70.4752591252, 70.4752594605}}, + {"e7ujxh", []float64{22.0825195312, 22.0880126953, -27.8173828125, -27.8063964844}}, + {"eb1zvy9n", []float64{1.39904022217, 1.39921188354, -8.53500366211, -8.53466033936}}, + {"1huxmt", []float64{-61.9793701172, -61.9738769531, -128.430175781, -128.419189453}}, + {"v4cyrgyg", []float64{61.5884971619, 61.5886688232, 47.8107833862, 47.811126709}}, + {"j6h0dvut", []float64{-78.6296653748, -78.6294937134, 62.0020294189, 62.0023727417}}, + {"r8j", []float64{-45.0, -43.59375, 164.53125, 165.9375}}, + {"feyj018b7", []float64{66.9809389114, 66.9809818268, -59.0613412857, -59.0612983704}}, + {"tw33887htf7j", []float64{35.4220805503, 35.422080718, 69.2841558158, 69.2841561511}}, + {"3rbgkj8z4cy8", []float64{-0.803537517786, -0.803537350148, -122.518374547, -122.518374212}}, + {"gq30gnn9dg", []float64{80.3213185072, 80.3213238716, -32.2028696537, -32.2028589249}}, + {"rek4qy2", []float64{-26.2889099121, -26.2875366211, 163.421630859, 163.42300415}}, + {"j8", []float64{-90.0, -84.375, 67.5, 78.75}}, + {"f2rev1d7", []float64{47.0741844177, 47.0743560791, -67.9803085327, -67.97996521}}, + {"rw1sg2f78x", []float64{-10.4102808237, -10.4102754593, 159.755308628, 159.755319357}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"8xfw", []float64{44.6484375, 44.82421875, -153.984375, -153.6328125}}, + {"fj20", []float64{74.53125, 74.70703125, -90.0, -89.6484375}}, + {"m76w867pt9yj", []float64{-25.5625145696, -25.562514402, 59.7809752822, 59.7809756175}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"wdb4v9vwt4ez", []float64{15.9628918581, 15.9628920257, 112.749471925, 112.74947226}}, + {"m53rvw", []float64{-25.3234863281, -25.3179931641, 46.9995117188, 47.0104980469}}, + {"f33", []float64{52.03125, 53.4375, -77.34375, -75.9375}}, + {"0t36bun98", []float64{-59.9631214142, -59.9630784988, -155.700302124, -155.700259209}}, + {"ezs1", []float64{42.36328125, 42.5390625, -5.625, -5.2734375}}, + {"jb", []float64{-90.0, -84.375, 78.75, 90.0}}, + {"vd0k", []float64{56.953125, 57.12890625, 67.8515625, 68.203125}}, + {"39cqr9bd", []float64{-34.0476608276, -34.0474891663, -110.411911011, -110.411567688}}, + {"n8r", []float64{-88.59375, -87.1875, 122.34375, 123.75}}, + {"1b6", []float64{-88.59375, -87.1875, -98.4375, -97.03125}}, + {"358wn7v3tch", []float64{-24.2369502783, -24.2369489372, -134.014754891, -134.01475355}}, + {"d506n2v77qf", []float64{17.2312764823, 17.2312778234, -89.366427362, -89.3664260209}}, + {"qb", []float64{-45.0, -39.375, 123.75, 135.0}}, + {"sn", []float64{33.75, 39.375, 0.0, 11.25}}, + {"5rj", []float64{-50.625, -49.21875, -26.71875, -25.3125}}, + {"0y51c", []float64{-55.9423828125, -55.8984375, -141.987304688, -141.943359375}}, + {"r85z8", []float64{-43.681640625, -43.6376953125, 162.7734375, 162.817382812}}, + {"rk5z", []float64{-21.26953125, -21.09375, 151.5234375, 151.875}}, + {"vzn2", []float64{84.375, 84.55078125, 87.5390625, 87.890625}}, + {"bjvk", []float64{78.046875, 78.22265625, -172.6171875, -172.265625}}, + {"b9f", []float64{54.84375, 56.25, -154.6875, -153.28125}}, + {"q0u1y3mu4k", []float64{-40.4660582542, -40.4660528898, 95.907651186, 95.9076619148}}, + {"r7", []float64{-28.125, -22.5, 146.25, 157.5}}, + {"cv90dz31", []float64{76.0653877258, 76.0655593872, -99.7215270996, -99.7211837769}}, + {"gxfeekgv9sng", []float64{89.2360430025, 89.2360431701, -18.8363294676, -18.8363291323}}, + {"92t", []float64{2.8125, 4.21875, -116.71875, -115.3125}}, + {"m06", []float64{-43.59375, -42.1875, 47.8125, 49.21875}}, + {"n27p3f3c", []float64{-87.306804657, -87.3066329956, 105.548057556, 105.548400879}}, + {"jjptjwvt2", []float64{-60.9581136703, -60.958070755, 55.7961273193, 55.7961702347}}, + {"u258hxwr", []float64{45.0424003601, 45.0425720215, 16.3782119751, 16.3785552979}}, + {"t47", []float64{12.65625, 14.0625, 49.21875, 50.625}}, + {"e", []float64{0.0, 45.0, -45.0, 0.0}}, + {"0s", []float64{-67.5, -61.875, -157.5, -146.25}}, + {"4drwu7v94g3", []float64{-76.1364381015, -76.1364367604, -56.758684963, -56.7586836219}}, + {"wk14pc", []float64{22.8570556641, 22.8625488281, 102.996826172, 103.0078125}}, + {"w5xg", []float64{20.21484375, 20.390625, 100.8984375, 101.25}}, + {"f2z2dvf7", []float64{49.3387413025, 49.3389129639, -68.4307479858, -68.4304046631}}, + {"duk0212sgtp", []float64{23.9579039812, 23.9579053223, -50.6241537631, -50.624152422}}, + {"h7z", []float64{-68.90625, -67.5, 21.09375, 22.5}}, + {"31k02yzy90", []float64{-37.8866100311, -37.8866046667, -129.331355095, -129.331344366}}, + {"vus0k", []float64{70.3564453125, 70.400390625, 84.55078125, 84.5947265625}}, + {"5m0sjm4rrvf", []float64{-61.1431337893, -61.1431324482, -32.8127369285, -32.8127355874}}, + {"4syd3n9", []float64{-62.8500366211, -62.8486633301, -58.3140563965, -58.3126831055}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"g", []float64{45.0, 90.0, -45.0, 0.0}}, + {"ntn69zcu", []float64{-61.392288208, -61.3921165466, 121.368370056, 121.368713379}}, + {"vnc4m", []float64{83.3642578125, 83.408203125, 46.6259765625, 46.669921875}}, + {"sy848jtk7wdj", []float64{37.0329307951, 37.0329309627, 33.7573626637, 33.757362999}}, + {"ry7y", []float64{-8.7890625, -8.61328125, 174.0234375, 174.375}}, + {"5k7gesef08j", []float64{-65.453453064, -65.4534517229, -28.3175759017, -28.3175745606}}, + {"n5gnev", []float64{-67.7362060547, -67.7307128906, 94.3835449219, 94.39453125}}, + {"kz3eeg1trf7", []float64{-3.58612284064, -3.58612149954, 36.0265664756, 36.0265678167}}, + {"t55rxfr7", []float64{18.2062339783, 18.2064056396, 49.9208450317, 49.9211883545}}, + {"tm9", []float64{30.9375, 32.34375, 57.65625, 59.0625}}, + {"pjw", []float64{-59.0625, -57.65625, 143.4375, 144.84375}}, + {"f", []float64{45.0, 90.0, -90.0, -45.0}}, + {"2my", []float64{-12.65625, -11.25, -160.3125, -158.90625}}, + {"3w6bwyt", []float64{-9.72015380859, -9.71878051758, -108.329315186, -108.327941895}}, + {"v4s8r3q", []float64{59.1133117676, 59.1146850586, 51.6549682617, 51.6563415527}}, + {"ggx8be488kyn", []float64{64.8359277472, 64.8359279148, -0.677700340748, -0.677700005472}}, + {"95w", []float64{19.6875, 21.09375, -126.5625, -125.15625}}, + {"9jck7spubf", []float64{33.1136190891, 33.1136244535, -133.077703714, -133.077692986}}, + {"f1zfe", []float64{55.283203125, 55.3271484375, -78.9697265625, -78.92578125}}, + {"p5ycnk9j", []float64{-68.7048912048, -68.7047195435, 144.768218994, 144.768562317}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"zk", []float64{67.5, 73.125, 146.25, 157.5}}, + {"rkq7z", []float64{-20.4345703125, -20.390625, 155.346679688, 155.390625}}, + {"j5wsc3t4", []float64{-69.4689559937, -69.4687843323, 54.2024230957, 54.2027664185}}, + {"56szw", []float64{-74.619140625, -74.5751953125, -26.806640625, -26.7626953125}}, + {"6xp21s7", []float64{-5.60165405273, -5.60028076172, -57.2346496582, -57.2332763672}}, + {"gbmymdgc5x", []float64{47.520198226, 47.5202035904, -2.91706323624, -2.9170525074}}, + {"bmm935md5m", []float64{74.7691994905, 74.769204855, -160.963987112, -160.963976383}}, + {"z2v55d8", []float64{49.7598266602, 49.7611999512, 153.435058594, 153.436431885}}, + {"q232vgb0", []float64{-43.4413146973, -43.4411430359, 103.260498047, 103.26084137}}, + {"b7kf7b5uz4", []float64{63.6775839329, 63.6775892973, -161.900067329, -161.900056601}}, + {"y6", []float64{56.25, 61.875, 101.25, 112.5}}, + {"3xdf2ts4d", []float64{-2.38635063171, -2.38630771637, -108.605260849, -108.605217934}}, + {"0szy1551", []float64{-62.2099113464, -62.2097396851, -146.553497314, -146.553153992}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"kk86s", []float64{-19.248046875, -19.2041015625, 11.77734375, 11.8212890625}}, + {"tqq", []float64{35.15625, 36.5625, 64.6875, 66.09375}}, + {"3gcy97b", []float64{-22.7430725098, -22.7416992188, -98.7341308594, -98.7327575684}}, + {"5dnz5z1d", []float64{-77.4807357788, -77.4805641174, -12.8409576416, -12.8406143188}}, + {"eumv1pe", []float64{24.8263549805, 24.8277282715, -3.11599731445, -3.11462402344}}, + {"2kxmbgqtfc3", []float64{-18.6579112709, -18.6579099298, -158.512682766, -158.512681425}}, + {"hc5uf211rpb", []float64{-83.5397829115, -83.5397815704, 39.1239881516, 39.1239894927}}, + {"p4khbd21", []float64{-76.496257782, -76.4960861206, 140.646972656, 140.647315979}}, + {"xuq", []float64{23.90625, 25.3125, 177.1875, 178.59375}}, + {"gn63cf91nhf", []float64{80.47779724, 80.4777985811, -41.7573997378, -41.7573983967}}, + {"5hd1nh3", []float64{-64.4883728027, -64.4869995117, -41.922454834, -41.921081543}}, + {"ustjn5", []float64{71.2078857422, 71.2133789062, 29.794921875, 29.8059082031}}, + {"btv2", []float64{77.34375, 77.51953125, -150.1171875, -149.765625}}, + {"7ds5rkh3u1", []float64{-30.3439325094, -30.343927145, -16.5503883362, -16.5503776073}}, + {"54", []float64{-78.75, -73.125, -45.0, -33.75}}, + {"7sr", []float64{-21.09375, -19.6875, -12.65625, -11.25}}, + {"kr552fbb", []float64{-5.03860473633, -5.03843307495, 15.5027389526, 15.5030822754}}, + {"8uc453nv4qq4", []float64{27.0766978338, 27.0766980015, -144.691553414, -144.691553079}}, + {"6n3m", []float64{-8.96484375, -8.7890625, -88.2421875, -87.890625}}, + {"rptzf8", []float64{-1.4501953125, -1.44470214844, 143.195800781, 143.206787109}}, + {"x63b", []float64{12.65625, 12.83203125, 148.7109375, 149.0625}}, + {"qwj7353", []float64{-10.6608581543, -10.6594848633, 119.928131104, 119.929504395}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"v6", []float64{56.25, 61.875, 56.25, 67.5}}, + {"5mpwtp", []float64{-60.6939697266, -60.6884765625, -22.9833984375, -22.9724121094}}, + {"ukgs", []float64{72.421875, 72.59765625, 16.171875, 16.5234375}}, + {"qxyb4g6qf", []float64{-1.3872385025, -1.38719558716, 122.116212845, 122.11625576}}, + {"qhww7zdb5v", []float64{-18.5476416349, -18.5476362705, 99.3093574047, 99.3093681335}}, + {"wfwffcw5vd", []float64{14.5547926426, 14.554798007, 133.37151289, 133.371523619}}, + {"rynp", []float64{-10.01953125, -9.84375, 177.1875, 177.5390625}}, + {"fb57ykxryn82", []float64{45.6852641702, 45.6852643378, -51.3948151097, -51.3948147744}}, + {"30sq", []float64{-41.1328125, -40.95703125, -129.0234375, -128.671875}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"e5gjz7", []float64{22.1209716797, 22.1264648438, -40.4626464844, -40.4516601562}}, + {"t6vkbx", []float64{16.3421630859, 16.34765625, 63.6547851562, 63.6657714844}}, + {"3e", []float64{-28.125, -22.5, -112.5, -101.25}}, + {"th", []float64{22.5, 28.125, 45.0, 56.25}}, + {"7j", []float64{-16.875, -11.25, -45.0, -33.75}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"1r15z2z5", []float64{-49.9611854553, -49.9610137939, -122.015533447, -122.015190125}}, + {"k9e57kg2f3", []float64{-35.9649842978, -35.9649789333, 26.866132021, 26.8661427498}}, + {"4w", []float64{-56.25, -50.625, -67.5, -56.25}}, + {"82c582qhsx7", []float64{4.83616903424, 4.83617037535, -167.324326783, -167.324325442}}, + {"qzdzkwwdc", []float64{-1.50190830231, -1.50186538696, 127.823910713, 127.823953629}}, + {"6xn26p6fnr0", []float64{-5.54084837437, -5.54084703326, -58.6190021038, -58.6190007627}}, + {"wm4y0xu8ee", []float64{29.2223614454, 29.2223668098, 105.14549017, 105.145500898}}, + {"rke5rewv", []float64{-19.0961265564, -19.095954895, 150.807609558, 150.807952881}}, + {"0bn3vwcjj", []float64{-89.6544456482, -89.6544027328, -137.217650414, -137.217607498}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"red", []float64{-25.3125, -23.90625, 160.3125, 161.71875}}, + {"r2", []float64{-45.0, -39.375, 146.25, 157.5}}, + {"v7qptt2u5k", []float64{64.6291565895, 64.6291619539, 64.9303686619, 64.9303793907}}, + {"pey", []float64{-68.90625, -67.5, 165.9375, 167.34375}}, + {"r7cg5cz8", []float64{-23.3692932129, -23.3691215515, 148.886032104, 148.886375427}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"qqr5mxz", []float64{-9.22988891602, -9.228515625, 111.345062256, 111.346435547}}, + {"wuuvdz7x2", []float64{27.7266168594, 27.7266597748, 130.555343628, 130.555386543}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"4mgwkd9yp3r5", []float64{-56.5428471006, -56.542846933, -73.6276473105, -73.6276469752}}, + {"dpk6jbemhyvh", []float64{41.1364542693, 41.1364544369, -83.7660782039, -83.7660778686}}, + {"768py6th4818", []float64{-29.5607757568, -29.5607755892, -33.4683660418, -33.4683657065}}, + {"q8", []float64{-45.0, -39.375, 112.5, 123.75}}, + {"xmgvmq9cf", []float64{33.3026075363, 33.3026504517, 151.756639481, 151.756682396}}, + {"6rhhdy3", []float64{-4.79965209961, -4.79827880859, -73.0027770996, -73.0014038086}}, + {"05dd6myj4xqj", []float64{-69.884508457, -69.8845082894, -176.377142966, -176.377142631}}, + {"yw6", []float64{80.15625, 81.5625, 115.3125, 116.71875}}, + {"6mt6h4jrw", []float64{-13.6986637115, -13.6986207962, -71.1839389801, -71.1838960648}}, + {"86ymqv2s", []float64{16.4211273193, 16.4212989807, -159.663619995, -159.663276672}}, + {"vygq9vr6umc", []float64{84.1406701505, 84.1406714916, 83.4073568881, 83.4073582292}}, + {"89g6sp8p81", []float64{10.3256946802, 10.3257000446, -152.75390625, -152.753895521}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"6st", []float64{-19.6875, -18.28125, -60.46875, -59.0625}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"sk", []float64{22.5, 28.125, 11.25, 22.5}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"f3pf69r61v4", []float64{51.0277444124, 51.0277457535, -67.7316650748, -67.7316637337}}, + {"r9qde0cj", []float64{-37.5243186951, -37.5241470337, 166.773834229, 166.774177551}}, + {"nbm5pqh", []float64{-88.0334472656, -88.0320739746, 131.10534668, 131.106719971}}, + {"u7f9fh2dzpw9", []float64{66.4252256043, 66.425225772, 14.8545113951, 14.8545117304}}, + {"pnzr19j29", []float64{-50.7952022552, -50.7951593399, 145.268483162, 145.268526077}}, + {"3e7rf8ww4fe5", []float64{-25.3526548482, -25.3526546806, -107.810775787, -107.810775451}}, + {"1x", []float64{-50.625, -45.0, -112.5, -101.25}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"4h4swejxpzv", []float64{-66.6912616789, -66.6912603378, -86.1908380687, -86.1908367276}}, + {"hy6gnf0u", []float64{-54.3047332764, -54.304561615, 37.9148483276, 37.9151916504}}, + {"xnycujudf", []float64{38.3084249496, 38.308467865, 144.67423439, 144.674277306}}, + {"t6bypmux9z54", []float64{16.5563485399, 16.5563487075, 57.6295499504, 57.6295502856}}, + {"ufw1c6xp2t", []float64{59.3851214647, 59.3851268291, 42.2520661354, 42.2520768642}}, + {"42jpxwe9byn", []float64{-88.6456024647, -88.6456011236, -71.3843134046, -71.3843120635}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"tj0de", []float64{28.564453125, 28.6083984375, 45.8349609375, 45.87890625}}, + {"e0kqd", []float64{2.548828125, 2.5927734375, -38.935546875, -38.8916015625}}, + {"bzysgj0rr", []float64{89.4574213028, 89.4574642181, -136.976895332, -136.976852417}}, + {"bxdp8ycgtcqt", []float64{88.543546591, 88.5435467586, -154.651882276, -154.651881941}}, + {"k0kx2785", []float64{-42.2995948792, -42.2994232178, 6.33911132812, 6.33945465088}}, + {"75ugg", []float64{-23.2470703125, -23.203125, -38.1884765625, -38.14453125}}, + {"sbbsbv2r", []float64{5.08375167847, 5.08392333984, 34.4864273071, 34.4867706299}}, + {"u7vunvq7rn", []float64{66.8263041973, 66.8263095617, 19.6414518356, 19.6414625645}}, + {"w4m7uexcx", []float64{13.3349132538, 13.3349561691, 97.591509819, 97.5915527344}}, + {"350g", []float64{-27.59765625, -27.421875, -133.9453125, -133.59375}}, + {"p", []float64{-90.0, -45.0, 135.0, 180.0}}, + {"t8w2g1m1", []float64{2.95137405396, 2.95154571533, 76.4277648926, 76.4281082153}}, + {"96k738f", []float64{13.2316589355, 13.2330322266, -117.704772949, -117.703399658}}, + {"c26nv174", []float64{47.5999832153, 47.6001548767, -120.713653564, -120.713310242}}, + {"s67g9ehvds", []float64{13.2889294624, 13.2889348269, 16.5959858894, 16.5959966183}}, + {"4ybt4sw", []float64{-51.1276245117, -51.1262512207, -55.4287719727, -55.4273986816}}, + {"5jqnz9zqyk12", []float64{-59.2714333534, -59.2714331858, -36.2226838991, -36.2226835638}}, + {"31d5nguy", []float64{-36.0135269165, -36.0133552551, -131.884346008, -131.884002686}}, + {"m4bbcg", []float64{-29.3829345703, -29.3774414062, 46.1315917969, 46.142578125}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"mkx0hzzq", []float64{-19.6438980103, -19.6437263489, 66.3124465942, 66.312789917}}, + {"9bv7x0", []float64{4.833984375, 4.83947753906, -93.5595703125, -93.5485839844}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"0x7pv0083h", []float64{-47.8563809395, -47.8563755751, -153.060793877, -153.060783148}}, + {"ruqzw0ztgw", []float64{-19.7702515125, -19.7702461481, 178.516309261, 178.51631999}}, + {"xpy5c3c", []float64{44.2625427246, 44.2639160156, 143.493804932, 143.495178223}}, + {"bnn6fxqm", []float64{79.2740821838, 79.2742538452, -171.09249115, -171.092147827}}, + {"fnm9u1t", []float64{80.4721069336, 80.4734802246, -82.0829772949, -82.0816040039}}, + {"jfvn54", []float64{-73.4655761719, -73.4600830078, 85.9130859375, 85.9240722656}}, + {"dj", []float64{28.125, 33.75, -90.0, -78.75}}, + {"mxfstk2s", []float64{-0.591201782227, -0.59103012085, 71.2470245361, 71.2473678589}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"4xy4p", []float64{-46.0546875, -46.0107421875, -58.7548828125, -58.7109375}}, + {"cbdg47d", []float64{48.3590698242, 48.3604431152, -97.2811889648, -97.2798156738}}, + {"1ddv9", []float64{-74.970703125, -74.9267578125, -108.588867188, -108.544921875}}, + {"4cn07cd", []float64{-84.3228149414, -84.3214416504, -47.6449584961, -47.6435852051}}, + {"dq8fjtv64n", []float64{36.9460237026, 36.946029067, -77.4463176727, -77.4463069439}}, + {"gx2qc4", []float64{86.9787597656, 86.9842529297, -22.1044921875, -22.0935058594}}, + {"yx", []float64{84.375, 90.0, 112.5, 123.75}}, + {"44", []float64{-78.75, -73.125, -90.0, -78.75}}, + {"zz679sbs", []float64{86.4232635498, 86.4234352112, 171.980667114, 171.981010437}}, + {"2fdh2u769u7y", []float64{-30.1666307822, -30.1666306145, -143.399997689, -143.399997354}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"11cxx", []float64{-78.837890625, -78.7939453125, -132.583007812, -132.5390625}}, + {"ygf2", []float64{66.09375, 66.26953125, 126.9140625, 127.265625}}, + {"m0uscc", []float64{-39.9407958984, -39.9353027344, 51.4050292969, 51.416015625}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"kn2m", []float64{-8.96484375, -8.7890625, 0.3515625, 0.703125}}, + {"k56d043m45", []float64{-26.3539534807, -26.3539481163, 3.51742744446, 3.51743817329}}, + {"m7", []float64{-28.125, -22.5, 56.25, 67.5}}, + {"pwe", []float64{-53.4375, -52.03125, 161.71875, 163.125}}, + {"1j7q0hs8u", []float64{-59.3892145157, -59.3891716003, -130.423336029, -130.423293114}}, + {"f264077wzn", []float64{46.776856184, 46.7768615484, -75.9214067459, -75.9213960171}}, + {"2jvn", []float64{-11.6015625, -11.42578125, -172.96875, -172.6171875}}, + {"4d4ghmzzsjb9", []float64{-78.1897520833, -78.1897519156, -63.4352295846, -63.4352292493}}, + {"1h2n6vef0we", []float64{-64.9645265937, -64.9645252526, -134.873975068, -134.873973727}}, + {"vnfc1v2yh", []float64{83.1744003296, 83.1744432449, 48.9452934265, 48.9453363419}}, + {"2b5brk", []float64{-44.9340820312, -44.9285888672, -140.657958984, -140.646972656}}, + {"yntb", []float64{81.5625, 81.73828125, 98.0859375, 98.4375}}, + {"5fj85yy", []float64{-78.7129211426, -78.7115478516, -3.34259033203, -3.34121704102}}, + {"cm36wevqn", []float64{74.9923324585, 74.9923753738, -121.699075699, -121.699032784}}, + {"7hf52n", []float64{-17.6770019531, -17.6715087891, -42.1875, -42.1765136719}}, + {"dh1sz", []float64{23.3349609375, 23.37890625, -87.5830078125, -87.5390625}}, + {"h", []float64{-90.0, -45.0, 0.0, 45.0}}, + {"7ny5k0k28", []float64{-6.4585018158, -6.45845890045, -36.3808822632, -36.3808393478}}, + {"w7", []float64{16.875, 22.5, 101.25, 112.5}}, + {"f9j0296wjz", []float64{50.6768792868, 50.6768846512, -60.443097353, -60.4430866241}}, + {"v316n9gu7y5", []float64{50.9869372845, 50.9869386256, 58.2987718284, 58.2987731695}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"0jt97bxt", []float64{-58.8391685486, -58.8389968872, -172.090530396, -172.090187073}}, + {"cfdbvrc", []float64{59.236907959, 59.23828125, -97.1507263184, -97.1493530273}}, + {"95d", []float64{19.6875, 21.09375, -132.1875, -130.78125}}, + {"my", []float64{-11.25, -5.625, 78.75, 90.0}}, + {"5t", []float64{-61.875, -56.25, -22.5, -11.25}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"k2j", []float64{-45.0, -43.59375, 18.28125, 19.6875}}, + {"rjygh", []float64{-12.12890625, -12.0849609375, 144.66796875, 144.711914062}}, + {"xs79f", []float64{24.2138671875, 24.2578125, 162.509765625, 162.553710938}}, + {"nmz3x305vedq", []float64{-57.3864214495, -57.3864212818, 111.764155068, 111.764155403}}, + {"u1hq", []float64{51.6796875, 51.85546875, 5.9765625, 6.328125}}, + {"2v8jpgs92zxm", []float64{-13.1641120277, -13.1641118601, -145.903202109, -145.903201774}}, + {"f54n04uq", []float64{62.9458236694, 62.9459953308, -87.1816635132, -87.1813201904}}, + {"8zkv13p8", []float64{41.6656494141, 41.6658210754, -139.505081177, -139.504737854}}, + {"z03j6ktm2", []float64{47.354722023, 47.3547649384, 136.512336731, 136.512379646}}, + {"qd266u", []float64{-31.9262695312, -31.9207763672, 112.972412109, 112.983398438}}, + {"783q5", []float64{-42.5390625, -42.4951171875, -20.6103515625, -20.56640625}}, + {"ynqe7fy5s9m6", []float64{80.7432531193, 80.7432532869, 99.3138598278, 99.3138601631}}, + {"pp", []float64{-50.625, -45.0, 135.0, 146.25}}, + {"bd", []float64{56.25, 61.875, -157.5, -146.25}}, + {"8573xzmt2qy", []float64{18.5856847465, 18.5856860876, -175.081539452, -175.081538111}}, + {"gwhbzrftu0", []float64{78.9253950119, 78.9254003763, -15.4981040955, -15.4980933666}}, + {"h42w9e805", []float64{-76.1819458008, -76.1819028854, 0.769171714783, 0.769214630127}}, + {"uzbhd66135d1", []float64{89.397358764, 89.3973589316, 33.8516691327, 33.851669468}}, + {"zf", []float64{56.25, 61.875, 168.75, 180.0}}, + {"6q2r", []float64{-8.61328125, -8.4375, -78.3984375, -78.046875}}, + {"qtxy7v4w9", []float64{-12.9352855682, -12.9352426529, 123.566708565, 123.56675148}}, + {"58", []float64{-90.0, -84.375, -22.5, -11.25}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"8u9qjb5qw", []float64{26.368303299, 26.3683462143, -144.234781265, -144.23473835}}, + {"48sx", []float64{-85.95703125, -85.78125, -61.171875, -60.8203125}}, + {"690tdt6", []float64{-38.3793640137, -38.3779907227, -66.6842651367, -66.6828918457}}, + {"qm8", []float64{-14.0625, -12.65625, 101.25, 102.65625}}, + {"2mj", []float64{-16.875, -15.46875, -161.71875, -160.3125}}, + {"3e5", []float64{-28.125, -26.71875, -108.28125, -106.875}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"f6dg1tndqxy", []float64{59.6177373827, 59.6177387238, -74.8076811433, -74.8076798022}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"9f3eqkhmf", []float64{13.2504987717, 13.250541687, -98.8600444794, -98.860001564}}, + {"yuqghk1x5z", []float64{69.4568055868, 69.4568109512, 133.431175947, 133.431186676}}, + {"w30rynrjmy", []float64{7.02257037163, 7.02257573605, 101.875094175, 101.875104904}}, + {"m25ergh6", []float64{-44.4118881226, -44.4117164612, 61.5182876587, 61.5186309814}}, + {"jqznhq", []float64{-50.9436035156, -50.9381103516, 66.2805175781, 66.2915039062}}, + {"3u4", []float64{-22.5, -21.09375, -98.4375, -97.03125}}, + {"fzr", []float64{85.78125, 87.1875, -46.40625, -45.0}}, + {"je", []float64{-73.125, -67.5, 67.5, 78.75}}, + {"pmztf5q7e7d4", []float64{-56.6270351037, -56.6270349361, 156.893490851, 156.893491186}}, + {"xdknsz", []float64{13.8372802734, 13.8427734375, 163.333740234, 163.344726562}}, + {"736h1c7d5h", []float64{-37.2583937645, -37.2583884001, -30.8556604385, -30.8556497097}}, + {"c57pmmv9u", []float64{64.5875501633, 64.5875930786, -130.542812347, -130.542769432}}, + {"80qnjsh", []float64{2.48291015625, 2.48428344727, -171.315307617, -171.313934326}}, + {"kp", []float64{-5.625, 0.0, 0.0, 11.25}}, + {"u6yufbthbdw", []float64{61.3072863221, 61.3072876632, 20.8699330688, 20.8699344099}}, + {"c4yj93f16ys", []float64{61.4454093575, 61.4454106987, -126.504698396, -126.504697055}}, + {"ygkp", []float64{64.51171875, 64.6875, 129.375, 129.7265625}}, + {"h1ypf", []float64{-78.7939453125, -78.75, 8.525390625, 8.5693359375}}, + {"hz76my5h4qe", []float64{-48.7895616889, -48.7895603478, 38.5772185028, 38.5772198439}}, + {"zh1cw", []float64{67.763671875, 67.8076171875, 137.724609375, 137.768554688}}, + {"00", []float64{-90.0, -84.375, -180.0, -168.75}}, + {"h9be5u0k", []float64{-79.6062469482, -79.6060752869, 23.3682632446, 23.3686065674}}, + {"4btv", []float64{-86.30859375, -86.1328125, -48.1640625, -47.8125}}, + {"42hz7pm83dt", []float64{-88.6857041717, -88.6857028306, -71.9308523834, -71.9308510423}}, + {"qv49", []float64{-16.69921875, -16.5234375, 127.265625, 127.6171875}}, + {"0nwzwd", []float64{-52.1081542969, -52.1026611328, -170.222167969, -170.211181641}}, + {"jhkmc", []float64{-65.0830078125, -65.0390625, 51.0205078125, 51.064453125}}, + {"sysens0py", []float64{37.1131467819, 37.1131896973, 40.3640270233, 40.3640699387}}, + {"5q792kbf", []float64{-54.5975875854, -54.5974159241, -28.8161087036, -28.8157653809}}, + {"624gbe", []float64{-44.3243408203, -44.3188476562, -74.8608398438, -74.8498535156}}, + {"gtkjfqg", []float64{75.5790710449, 75.5804443359, -16.7720031738, -16.7706298828}}, + {"nv4", []float64{-61.875, -60.46875, 126.5625, 127.96875}}, + {"dcwv6uu0", []float64{9.3864440918, 9.38661575317, -46.6314697266, -46.6311264038}}, + {"vvgtyfv9", []float64{78.36977005, 78.3699417114, 83.97605896, 83.9764022827}}, + {"53rjpqr6zt9", []float64{-82.0550099015, -82.0550085604, -23.5773669183, -23.5773655772}}, + {"vmyp4dnemp6", []float64{78.5858018696, 78.5858032107, 64.8065069318, 64.8065082729}}, + {"t9xqjxjpv", []float64{9.53197002411, 9.53201293945, 77.9440927505, 77.9441356659}}, + {"sby32e", []float64{4.45495605469, 4.46044921875, 42.5610351562, 42.5720214844}}, + {"sjfgy", []float64{33.0029296875, 33.046875, 4.130859375, 4.1748046875}}, + {"k7q0z2b2du", []float64{-26.5826869011, -26.5826815367, 20.0065648556, 20.0065755844}}, + {"nt", []float64{-61.875, -56.25, 112.5, 123.75}}, + {"1", []float64{-90.0, -45.0, -135.0, -90.0}}, + {"mpfpfd32e", []float64{-0.0314998626709, -0.0314569473267, 47.9242086411, 47.9242515564}}, + {"hqjqn", []float64{-55.1953125, -55.1513671875, 18.896484375, 18.9404296875}}, + {"9q7chj6u2uw", []float64{35.3616240621, 35.3616254032, -118.296964467, -118.296963125}}, + {"0wsf47v9c", []float64{-53.0650377274, -53.064994812, -150.713839531, -150.713796616}}, + {"kdv72env8xke", []float64{-28.9424979128, -28.9424977452, 29.9140823632, 29.9140826985}}, + {"trfx", []float64{44.82421875, 45.0, 59.765625, 60.1171875}}, + {"02uttm", []float64{-84.7869873047, -84.7814941406, -162.191162109, -162.180175781}}, + {"hhjgb5s3vv39", []float64{-66.8212655, -66.8212653324, 8.0920227617, 8.09202309698}}, + {"r16", []float64{-37.96875, -36.5625, 137.8125, 139.21875}}, + {"4xy44eer4t3", []float64{-46.0342316329, -46.0342302918, -58.9480648935, -58.9480635524}}, + {"8b", []float64{0.0, 5.625, -146.25, -135.0}}, + {"zd", []float64{56.25, 61.875, 157.5, 168.75}}, + {"z0x", []float64{47.8125, 49.21875, 144.84375, 146.25}}, + {"4967", []float64{-82.44140625, -82.265625, -64.3359375, -63.984375}}, + {"2vf4", []float64{-12.3046875, -12.12890625, -143.4375, -143.0859375}}, + {"tzp3t0rtg", []float64{39.6410322189, 39.6410751343, 89.1754674911, 89.1755104065}}, + {"75yry", []float64{-22.5439453125, -22.5, -35.947265625, -35.9033203125}}, + {"bdgtu", []float64{61.4794921875, 61.5234375, -152.40234375, -152.358398438}}, + {"u1", []float64{50.625, 56.25, 0.0, 11.25}}, + {"rz2bgp7hds", []float64{-4.04629468918, -4.04628932476, 169.940750599, 169.940761328}}, + {"g", []float64{45.0, 90.0, -45.0, 0.0}}, + {"psptppx32e", []float64{-66.5796643496, -66.5796589851, 168.364470005, 168.364480734}}, + {"gshfctpwf1", []float64{68.0120283365, 68.0120337009, -15.7440090179, -15.7439982891}}, + {"3yq", []float64{-9.84375, -8.4375, -92.8125, -91.40625}}, + {"685zv36e0fd2", []float64{-43.6303004622, -43.6303002946, -61.9923811778, -61.9923808426}}, + {"7gf5fd", []float64{-23.2360839844, -23.2305908203, -8.32763671875, -8.31665039062}}, + {"bmmzfq", []float64{75.9265136719, 75.9320068359, -160.565185547, -160.554199219}}, + {"m40", []float64{-33.75, -32.34375, 45.0, 46.40625}}, + {"tx45501g7v", []float64{39.9029284716, 39.902933836, 70.4469001293, 70.4469108582}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"ej054jn", []float64{28.6798095703, 28.6811828613, -44.9038696289, -44.9024963379}}, + {"n1g7d", []float64{-79.541015625, -79.4970703125, 94.658203125, 94.7021484375}}, + {"nn6ejehuf", []float64{-54.2991113663, -54.2990684509, 93.7639331818, 93.7639760971}}, + {"qs8e93xwrrc", []float64{-19.0629114211, -19.06291008, 113.268668801, 113.268670142}}, + {"f2", []float64{45.0, 50.625, -78.75, -67.5}}, + {"gm", []float64{73.125, 78.75, -33.75, -22.5}}, + {"npp4rnm", []float64{-50.1951599121, -50.1937866211, 100.158233643, 100.159606934}}, + {"6t", []float64{-16.875, -11.25, -67.5, -56.25}}, + {"2f4fe3d5", []float64{-33.3017921448, -33.3016204834, -142.237243652, -142.23690033}}, + {"s7r00k0196", []float64{18.3034908772, 18.3034962416, 21.1047899723, 21.1048007011}}, + {"st084", []float64{28.125, 28.1689453125, 23.291015625, 23.3349609375}}, + {"p6f3bv9f1", []float64{-74.1930770874, -74.1930341721, 149.449467659, 149.449510574}}, + {"fgk5j", []float64{63.80859375, 63.8525390625, -50.4052734375, -50.361328125}}, + {"yeu22jjtp", []float64{66.1660194397, 66.166062355, 118.484416008, 118.484458923}}, + {"3bcn7gkusn25", []float64{-39.6639578976, -39.6639577299, -99.6722602844, -99.6722599491}}, + {"1", []float64{-90.0, -45.0, -135.0, -90.0}}, + {"6q4dh71sqn", []float64{-10.8811962605, -10.881190896, -75.0452899933, -75.0452792645}}, + {"p", []float64{-90.0, -45.0, 135.0, 180.0}}, + {"6uy2kbef9yg", []float64{-18.2340927422, -18.2340914011, -47.2469682992, -47.246966958}}, + {"58j", []float64{-90.0, -88.59375, -15.46875, -14.0625}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"mh6x", []float64{-19.86328125, -19.6875, 48.515625, 48.8671875}}, + {"cgq4xrjz", []float64{63.7603569031, 63.7605285645, -92.486000061, -92.4856567383}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"ppqg570sk3n", []float64{-48.6741918325, -48.6741904914, 144.635886848, 144.635888189}}, + {"1suyu1k", []float64{-62.0878601074, -62.0864868164, -105.639038086, -105.637664795}}, + {"xm4xkzsnvux", []float64{29.4417956471, 29.4417969882, 149.980114549, 149.980115891}}, + {"8p3xj", []float64{42.01171875, 42.0556640625, -177.670898438, -177.626953125}}, + {"ef92nkk", []float64{14.0858459473, 14.0872192383, -9.21203613281, -9.2106628418}}, + {"qnrf101", []float64{-9.4921875, -9.49081420898, 100.943756104, 100.945129395}}, + {"2qt", []float64{-8.4375, -7.03125, -161.71875, -160.3125}}, + {"c2q7e", []float64{47.021484375, 47.0654296875, -114.829101562, -114.78515625}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"j6t0bwpjpusg", []float64{-75.7718221284, -75.7718219608, 63.3131746575, 63.3131749928}}, + {"wrq", []float64{40.78125, 42.1875, 109.6875, 111.09375}}, + {"xvf2jk", []float64{32.3657226562, 32.3712158203, 172.144775391, 172.155761719}}, + {"xy0p5y", []float64{35.0134277344, 35.0189208984, 168.914794922, 168.92578125}}, + {"bsh9xbd", []float64{67.766418457, 67.767791748, -150.828552246, -150.827178955}}, + {"g675yc", []float64{58.3209228516, 58.3264160156, -29.2346191406, -29.2236328125}}, + {"dkrnq7", []float64{25.0213623047, 25.0268554688, -68.6315917969, -68.6206054688}}, + {"6q4uk3dyk5", []float64{-10.4936009645, -10.4935956001, -74.6920967102, -74.6920859814}}, + {"t58tp1kxb54p", []float64{20.5746203475, 20.5746205151, 46.0169246793, 46.0169250146}}, + {"bbw73yzeuy", []float64{48.4215438366, 48.421549201, -137.373529673, -137.373518944}}, + {"gnq", []float64{80.15625, 81.5625, -36.5625, -35.15625}}, + {"3j", []float64{-16.875, -11.25, -135.0, -123.75}}, + {"7dx2c", []float64{-30.8056640625, -30.76171875, -12.2607421875, -12.216796875}}, + {"vn9", []float64{81.5625, 82.96875, 46.40625, 47.8125}}, + {"4kj", []float64{-67.5, -66.09375, -71.71875, -70.3125}}, + {"cuvj4trg5nb8", []float64{72.6270465553, 72.6270467229, -94.0981142968, -94.0981139615}}, + {"uetmbuswe", []float64{65.7240772247, 65.7241201401, 29.92208004, 29.9221229553}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"f", []float64{45.0, 90.0, -90.0, -45.0}}, + {"jg", []float64{-73.125, -67.5, 78.75, 90.0}}, + {"ycz", []float64{54.84375, 56.25, 133.59375, 135.0}}, + {"pevtd", []float64{-67.939453125, -67.8955078125, 165.322265625, 165.366210938}}, + {"gf7fm3hmb8", []float64{58.0582380295, 58.0582433939, -5.73999166489, -5.73998093605}}, + {"w7zwjnh24qd", []float64{22.1814313531, 22.1814326942, 112.022537291, 112.022538632}}, + {"nfesgy", []float64{-75.0695800781, -75.0640869141, 128.836669922, 128.84765625}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"efq0q0dg0g0n", []float64{12.7034739777, 12.7034741454, -2.5450193882, -2.54501905292}}, + {"kkucr8pk", []float64{-18.060836792, -18.0606651306, 18.2692337036, 18.2695770264}}, + {"5zdg4zvz", []float64{-47.2413825989, -47.2412109375, -7.25406646729, -7.25372314453}}, + {"fuw", []float64{70.3125, 71.71875, -47.8125, -46.40625}}, + {"x51mnftp8", []float64{17.7689266205, 17.7689695358, 137.061309814, 137.06135273}}, + {"y0", []float64{45.0, 50.625, 90.0, 101.25}}, + {"ndufku4sr", []float64{-74.1130399704, -74.1129970551, 119.392161369, 119.392204285}}, + {"ydwndhywhg8", []float64{60.232219398, 60.2322207391, 121.034520864, 121.034522206}}, + {"gj6ehkq0", []float64{75.0819396973, 75.0821113586, -41.2893676758, -41.289024353}}, + {"m3hfct0", []float64{-38.8641357422, -38.8627624512, 62.9956054688, 62.9969787598}}, + {"6745yupp70qu", []float64{-27.4426010996, -27.442600932, -75.631118305, -75.6311179698}}, + {"d7b0m9dzj213", []float64{21.1471368559, 21.1471370235, -78.504297249, -78.5042969137}}, + {"py", []float64{-56.25, -50.625, 168.75, 180.0}}, + {"4vhrpypw", []float64{-60.6105422974, -60.610370636, -49.9225616455, -49.9222183228}}, + {"xwyyj1xz5kzw", []float64{39.0329053625, 39.0329055302, 167.222706601, 167.222706936}}, + {"18ht0j2w8ste", []float64{-89.0911141969, -89.0911140293, -106.171159521, -106.171159185}}, + {"vynwqurve", []float64{79.8729228973, 79.8729658127, 88.1980276108, 88.1980705261}}, + {"s77hhn", []float64{19.0173339844, 19.0228271484, 15.64453125, 15.6555175781}}, + {"hj66tgs86", []float64{-60.0100278854, -60.0099849701, 3.42301368713, 3.42305660248}}, + {"e5nh4k", []float64{17.6000976562, 17.6055908203, -36.4636230469, -36.4526367188}}, + {"jk", []float64{-67.5, -61.875, 56.25, 67.5}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"f0p3v5", []float64{45.3240966797, 45.3295898438, -79.5849609375, -79.5739746094}}, + {"numc175r1", []float64{-65.9002876282, -65.9002447128, 131.895375252, 131.895418167}}, + {"7pc", []float64{-1.40625, 0.0, -43.59375, -42.1875}}, + {"b7qw82mfqc4", []float64{64.4255930185, 64.4255943596, -159.590199888, -159.590198547}}, + {"qfe", []float64{-30.9375, -29.53125, 127.96875, 129.375}}, + {"mw9kj6nrue61", []float64{-7.72204069421, -7.72204052657, 69.4973042607, 69.497304596}}, + {"6en5d6psj", []float64{-27.4980926514, -27.498049736, -58.9531087875, -58.9530658722}}, + {"mk80", []float64{-19.6875, -19.51171875, 56.25, 56.6015625}}, + {"d2fbpmpyjv", []float64{4.24727261066, 4.24727797508, -74.5533192158, -74.5533084869}}, + {"pf84wbwguh9", []float64{-75.4946324229, -75.4946310818, 169.056073576, 169.056074917}}, + {"ncj8287", []float64{-84.3296813965, -84.3283081055, 131.510467529, 131.51184082}}, + {"smd4t", []float64{31.376953125, 31.4208984375, 14.2822265625, 14.326171875}}, + {"4ryj3jjxrd", []float64{-45.4546773434, -45.454671979, -70.2606797218, -70.260668993}}, + {"udffsxnn8", []float64{60.9477710724, 60.9478139877, 26.5731811523, 26.5732240677}}, + {"cub7vr", []float64{72.4163818359, 72.421875, -100.667724609, -100.656738281}}, + {"y7c6s4", []float64{66.5441894531, 66.5496826172, 103.18359375, 103.194580078}}, + {"t253", []float64{0.17578125, 0.3515625, 60.8203125, 61.171875}}, + {"1e2bhmk9ybw", []float64{-71.6896077991, -71.6896064579, -111.252067387, -111.252066046}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"99r75", []float64{7.55859375, 7.6025390625, -102.172851562, -102.12890625}}, + {"knbr2kzz1791", []float64{-5.72952283546, -5.72952266783, 0.373246818781, 0.373247154057}}, + {"v8h", []float64{45.0, 46.40625, 73.125, 74.53125}}, + {"sm6xvf3bc", []float64{30.9060430527, 30.906085968, 15.0207567215, 15.0207996368}}, + {"vu", []float64{67.5, 73.125, 78.75, 90.0}}, + {"w56htc6", []float64{19.0791320801, 19.0805053711, 93.0679321289, 93.0693054199}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"5hm57j8", []float64{-65.4922485352, -65.4908752441, -37.8369140625, -37.8355407715}}, + {"k8qwr0e", []float64{-42.4923706055, -42.4909973145, 31.9523620605, 31.9537353516}}, + {"716e0", []float64{-37.44140625, -37.3974609375, -41.484375, -41.4404296875}}, + {"wz71b", []float64{41.0888671875, 41.1328125, 127.96875, 128.012695312}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"89x2", []float64{8.4375, 8.61328125, -147.3046875, -146.953125}}, + {"rcr37p", []float64{-37.7105712891, -37.705078125, 179.077148438, 179.088134766}}, + {"4xzjc7vmstr", []float64{-45.3739361465, -45.3739348054, -57.5939060748, -57.5939047337}}, + {"tv07ndf8", []float64{28.6674499512, 28.6676216125, 79.3906402588, 79.3909835815}}, + {"qb2z", []float64{-42.36328125, -42.1875, 124.8046875, 125.15625}}, + {"xjq0fjferr", []float64{29.6952670813, 29.6952724457, 143.529134989, 143.529145718}}, + {"zwn6", []float64{79.1015625, 79.27734375, 166.2890625, 166.640625}}, + {"7xc", []float64{-1.40625, 0.0, -21.09375, -19.6875}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"vw4sy8z", []float64{79.5890808105, 79.5904541016, 71.3108825684, 71.3122558594}}, + {"djg8s3pre", []float64{32.4384212494, 32.4384641647, -84.881272316, -84.8812294006}}, + {"vpn8t", []float64{84.462890625, 84.5068359375, 54.3603515625, 54.404296875}}, + {"1sse8x6", []float64{-64.0324401855, -64.0310668945, -106.147155762, -106.145782471}}, + {"snm4", []float64{35.5078125, 35.68359375, 7.03125, 7.3828125}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"5rb", []float64{-46.40625, -45.0, -33.75, -32.34375}}, + {"q7", []float64{-28.125, -22.5, 101.25, 112.5}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"hn5sw9rbvjk3", []float64{-55.4519608431, -55.4519606754, 5.21838281304, 5.21838314831}}, + {"y0f7w5tep1f", []float64{49.8537348211, 49.8537361622, 93.4355905652, 93.4355919063}}, + {"xts0uk", []float64{31.0913085938, 31.0968017578, 163.311767578, 163.322753906}}, + {"ftybqfey", []float64{77.4024581909, 77.4026298523, -57.7060317993, -57.7056884766}}, + {"eqvhf", []float64{38.8037109375, 38.84765625, -26.630859375, -26.5869140625}}, + {"ctpbp73", []float64{73.1428527832, 73.1442260742, -101.281585693, -101.280212402}}, + {"15czhy35", []float64{-67.6409339905, -67.6407623291, -132.328948975, -132.328605652}}, + {"1", []float64{-90.0, -45.0, -135.0, -90.0}}, + {"vk6mwwrysbf", []float64{69.9084989727, 69.9085003138, 59.7105565667, 59.7105579078}}, + {"4yfcbqvevqy", []float64{-51.6858740151, -51.685872674, -52.3640397191, -52.364038378}}, + {"d6qm1q41g", []float64{13.5684156418, 13.5684585571, -69.9031305313, -69.903087616}}, + {"kjqew1cty", []float64{-14.842915535, -14.8428726196, 9.40661430359, 9.40665721893}}, + {"hf9zn39ewerv", []float64{-74.6981724165, -74.6981722489, 36.4879449829, 36.4879453182}}, + {"1j", []float64{-61.875, -56.25, -135.0, -123.75}}, + {"u41", []float64{56.25, 57.65625, 1.40625, 2.8125}}, + {"pd8sbu5sk", []float64{-75.0798368454, -75.0797939301, 158.241062164, 158.24110508}}, + {"k7", []float64{-28.125, -22.5, 11.25, 22.5}}, + {"fx6xcm", []float64{87.1710205078, 87.1765136719, -63.9294433594, -63.9184570312}}, + {"k1nwc4mun", []float64{-38.1754302979, -38.1753873825, 9.19272422791, 9.19276714325}}, + {"nechx1mg", []float64{-68.1078529358, -68.1076812744, 114.221763611, 114.222106934}}, + {"8et6dbj4g", []float64{20.1274251938, 20.1274681091, -149.98934269, -149.989299774}}, + {"7e", []float64{-28.125, -22.5, -22.5, -11.25}}, + {"vqcthybtw0", []float64{83.885679245, 83.8856846094, 58.5690593719, 58.5690701008}}, + {"r6qdv32n", []float64{-31.8524551392, -31.8522834778, 155.621337891, 155.621681213}}, + {"tbhh", []float64{0.703125, 0.87890625, 84.375, 84.7265625}}, + {"0c5fpu", []float64{-84.0014648438, -83.9959716797, -140.635986328, -140.625}}, + {"7b", []float64{-45.0, -39.375, -11.25, 0.0}}, + {"9vzmkfvug", []float64{33.2825231552, 33.2825660706, -90.8379220963, -90.8378791809}}, + {"68t", []float64{-42.1875, -40.78125, -60.46875, -59.0625}}, + {"ef1szshm45h", []float64{12.1078079939, 12.107809335, -8.80510747433, -8.80510613322}}, + {"21dgj4", []float64{-36.0241699219, -36.0186767578, -175.913085938, -175.902099609}}, + {"109q9yt", []float64{-86.0092163086, -86.0078430176, -133.158416748, -133.157043457}}, + {"nhj3b9vc", []float64{-67.182598114, -67.1824264526, 97.4126815796, 97.4130249023}}, + {"nye5uzgb", []float64{-52.735748291, -52.7355766296, 128.182640076, 128.182983398}}, + {"dhz5f", []float64{27.3779296875, 27.421875, -80.068359375, -80.0244140625}}, + {"g1verehd", []float64{55.4318618774, 55.4320335388, -36.9298553467, -36.9295120239}}, + {"jtr", []float64{-60.46875, -59.0625, 77.34375, 78.75}}, + {"m5nbruj", []float64{-28.0590820312, -28.0577087402, 54.839630127, 54.841003418}}, + {"p", []float64{-90.0, -45.0, 135.0, 180.0}}, + {"h", []float64{-90.0, -45.0, 0.0, 45.0}}, + {"bm3gr", []float64{75.1025390625, 75.146484375, -165.981445312, -165.9375}}, + {"e7my1m0qp", []float64{19.3644332886, 19.3644762039, -25.6084871292, -25.6084442139}}, + {"fzue", []float64{89.12109375, 89.296875, -49.921875, -49.5703125}}, + {"q70", []float64{-28.125, -26.71875, 101.25, 102.65625}}, + {"sjeed", []float64{31.552734375, 31.5966796875, 5.009765625, 5.0537109375}}, + {"cvsuyyw", []float64{76.8081665039, 76.8095397949, -94.2654418945, -94.2640686035}}, + {"7dnp", []float64{-32.51953125, -32.34375, -14.0625, -13.7109375}}, + {"tf9kr1u", []float64{14.8191833496, 14.8205566406, 80.8209228516, 80.8222961426}}, + {"j38nduwqew", []float64{-80.3940546513, -80.3940492868, 56.3795828819, 56.3795936108}}, + {"444y82r", []float64{-77.606048584, -77.604675293, -86.1122131348, -86.1108398438}}, + {"1rwzsww", []float64{-46.4584350586, -46.4570617676, -114.051818848, -114.050445557}}, + {"98vu", []float64{4.921875, 5.09765625, -104.4140625, -104.0625}}, + {"f0hu79k2y84", []float64{45.7540655136, 45.7540668547, -83.1603857875, -83.1603844464}}, + {"35399zm2gnn", []float64{-26.415091753, -26.4150904119, -132.806374133, -132.806372792}}, + {"qzxy", []float64{-1.7578125, -1.58203125, 134.6484375, 135.0}}, + {"7gpr25", []float64{-26.8341064453, -26.8286132812, -1.0546875, -1.04370117188}}, + {"xucdp", []float64{27.0703125, 27.1142578125, 171.166992188, 171.2109375}}, + {"db3mpnz89zq", []float64{2.32235983014, 2.32236117125, -54.1741874814, -54.1741861403}}, + {"p", []float64{-90.0, -45.0, 135.0, 180.0}}, + {"94m52", []float64{13.2275390625, 13.271484375, -127.96875, -127.924804688}}, + {"u7ucp", []float64{66.26953125, 66.3134765625, 18.2373046875, 18.28125}}, + {"81qq43p", []float64{8.09143066406, 8.09280395508, -171.10244751, -171.101074219}}, + {"f80w8", []float64{46.142578125, 46.1865234375, -66.796875, -66.7529296875}}, + {"8j5z", []float64{29.35546875, 29.53125, -174.7265625, -174.375}}, + {"56q", []float64{-77.34375, -75.9375, -25.3125, -23.90625}}, + {"b72vvhj", []float64{64.3139648438, 64.3153381348, -167.468719482, -167.467346191}}, + {"5j", []float64{-61.875, -56.25, -45.0, -33.75}}, + {"42hm9tj0rn", []float64{-89.0056622028, -89.0056568384, -72.7003526688, -72.7003419399}}, + {"cbxx9q2c89", []float64{49.1654545069, 49.1654598713, -90.6471419334, -90.6471312046}}, + {"43", []float64{-84.375, -78.75, -78.75, -67.5}}, + {"rvmw", []float64{-14.4140625, -14.23828125, 176.484375, 176.8359375}}, + {"jwmeyr4hj7", []float64{-54.1454154253, -54.1454100609, 75.5120050907, 75.5120158195}}, + {"b3y", []float64{54.84375, 56.25, -160.3125, -158.90625}}, + {"4y3n0e7u8j", []float64{-53.7704104185, -53.7704050541, -54.8166275024, -54.8166167736}}, + {"m0k0x", []float64{-43.505859375, -43.4619140625, 50.9326171875, 50.9765625}}, + {"2zc1v219ev8z", []float64{-1.09834464267, -1.09834447503, -144.610815234, -144.610814899}}, + {"3rvj3ezyffez", []float64{-0.46162577346, -0.461625605822, -116.64206598, -116.642065644}}, + {"35bdpq", []float64{-23.5217285156, -23.5162353516, -133.978271484, -133.967285156}}, + {"qdqrzp2e7b", []float64{-30.9410619736, -30.9410566092, 121.597527266, 121.597537994}}, + {"vmrsrejf", []float64{75.2951431274, 75.2953147888, 67.1343612671, 67.1347045898}}, + {"up", []float64{84.375, 90.0, 0.0, 11.25}}, + {"bzy", []float64{88.59375, 90.0, -137.8125, -136.40625}}, + {"3rnm42gs62", []float64{-4.7412443161, -4.74123895168, -114.857157469, -114.85714674}}, + {"yhekty3621c", []float64{71.1382435262, 71.1382448673, 94.8247160017, 94.8247173429}}, + {"ektx", []float64{26.54296875, 26.71875, -26.015625, -25.6640625}}, + {"9nxkb4u9f6", []float64{37.4128782749, 37.4128836393, -124.798411131, -124.798400402}}, + {"fg", []float64{61.875, 67.5, -56.25, -45.0}}, + {"66e4x10k68", []float64{-30.4918241501, -30.4918187857, -74.2231822014, -74.2231714725}}, + {"me", []float64{-28.125, -22.5, 67.5, 78.75}}, + {"r385f5q9", []float64{-35.8852958679, -35.8851242065, 146.346817017, 146.347160339}}, + {"xbdc8wgn0", []float64{3.11428070068, 3.11432361603, 172.643280029, 172.643322945}}, + {"74s", []float64{-30.9375, -29.53125, -39.375, -37.96875}}, + {"dg8t7", []float64{20.6103515625, 20.654296875, -55.4150390625, -55.37109375}}, + {"nf7", []float64{-77.34375, -75.9375, 127.96875, 129.375}}, + {"6nzfqpxy", []float64{-6.59351348877, -6.59334182739, -78.8272476196, -78.8269042969}}, + {"0ux06p9jktht", []float64{-64.6014270745, -64.6014269069, -136.31678693, -136.316786595}}, + {"nb8pxznpjh", []float64{-85.8294653893, -85.8294600248, 124.099030495, 124.099041224}}, + {"6qks2x97hzm", []float64{-9.05492708087, -9.05492573977, -72.3979751766, -72.3979738355}}, + {"us6qrd43em", []float64{70.0161534548, 70.0161588192, 25.9968817234, 25.9968924522}}, + {"tp2eh", []float64{41.30859375, 41.3525390625, 45.87890625, 45.9228515625}}, + {"vcgf16q2", []float64{55.2076721191, 55.2078437805, 84.0869522095, 84.0872955322}}, + {"qt15nkc82kz", []float64{-16.3214953244, -16.3214939833, 114.182988256, 114.182989597}}, + {"t6t", []float64{14.0625, 15.46875, 63.28125, 64.6875}}, + {"yx53b3kj32", []float64{84.6903848648, 84.6903902292, 117.086845636, 117.086856365}}, + {"twqdxev4cx", []float64{35.6168121099, 35.6168174744, 76.9771456718, 76.9771564007}}, + {"p", []float64{-90.0, -45.0, 135.0, 180.0}}, + {"4gty084hgddy", []float64{-69.2569826916, -69.2569825239, -48.13918937, -48.1391890347}}, + {"c7", []float64{61.875, 67.5, -123.75, -112.5}}, + {"ywffc641", []float64{83.463306427, 83.4634780884, 116.424865723, 116.425209045}}, + {"k0km", []float64{-42.71484375, -42.5390625, 5.9765625, 6.328125}}, + {"17k4fg", []float64{-71.2188720703, -71.2133789062, -118.004150391, -117.993164062}}, + {"9fr", []float64{12.65625, 14.0625, -91.40625, -90.0}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"h08scsx", []float64{-86.3278198242, -86.3264465332, 0.778656005859, 0.780029296875}}, + {"8f8nq48", []float64{15.1748657227, 15.1762390137, -145.986328125, -145.984954834}}, + {"hecr6qx49k0", []float64{-67.59567976, -67.5956784189, 24.3663561344, 24.3663574755}}, + {"jn", []float64{-56.25, -50.625, 45.0, 56.25}}, + {"qwx7j", []float64{-7.91015625, -7.8662109375, 122.915039062, 122.958984375}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"wxcj4", []float64{44.47265625, 44.5166015625, 113.994140625, 114.038085938}}, + {"gw63h", []float64{80.33203125, 80.3759765625, -19.16015625, -19.1162109375}}, + {"hp7b6f27tq6p", []float64{-49.1618095525, -49.1618093848, 5.3948584199, 5.39485875517}}, + {"kd", []float64{-33.75, -28.125, 22.5, 33.75}}, + {"fbweu", []float64{48.4716796875, 48.515625, -46.93359375, -46.8896484375}}, + {"m1fcuue7nm1g", []float64{-34.8233712651, -34.8233710974, 49.080661498, 49.0806618333}}, + {"h9j", []float64{-84.375, -82.96875, 29.53125, 30.9375}}, + {"n9d3g5uv", []float64{-81.2334251404, -81.233253479, 115.80242157, 115.802764893}}, + {"nhpp1spf", []float64{-66.247215271, -66.2470436096, 99.9203109741, 99.9206542969}}, + {"7jg2w13b23h", []float64{-12.5614446402, -12.5614432991, -40.1635962725, -40.1635949314}}, + {"6q4z0ebf3", []float64{-9.99854564667, -9.99850273132, -74.8597669601, -74.8597240448}}, + {"sv9vqgeecr", []float64{31.8802589178, 31.8802642822, 36.5124285221, 36.5124392509}}, + {"wqd4", []float64{36.9140625, 37.08984375, 104.0625, 104.4140625}}, + {"bwqgj", []float64{80.68359375, 80.7275390625, -147.788085938, -147.744140625}}, + {"hk73qx", []float64{-65.8355712891, -65.830078125, 16.1059570312, 16.1169433594}}, + {"gdx1d6hr76v", []float64{59.3384175003, 59.3384188414, -12.5513903797, -12.5513890386}}, + {"47czd", []float64{-67.587890625, -67.5439453125, -76.201171875, -76.1572265625}}, + {"1kpebdk50", []float64{-66.8279457092, -66.8279027939, -113.17565918, -113.175616264}}, + {"g3z7", []float64{55.37109375, 55.546875, -23.5546875, -23.203125}}, + {"8x", []float64{39.375, 45.0, -157.5, -146.25}}, + {"nczvrs", []float64{-79.2114257812, -79.2059326172, 134.978027344, 134.989013672}}, + {"fbyjmwc6", []float64{50.1790237427, 50.1791954041, -47.5690841675, -47.5687408447}}, + {"yhz41tus5wm", []float64{72.1026183665, 72.1026197076, 99.9160046875, 99.9160060287}}, + {"uktff1pp0", []float64{70.8025932312, 70.8026361465, 19.4334411621, 19.4334840775}}, + {"hrt8", []float64{-47.8125, -47.63671875, 18.984375, 19.3359375}}, + {"b5vkzheshz3", []float64{66.9541557133, 66.9541570544, -172.304558605, -172.304557264}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"h6mvgwv1kmp", []float64{-76.2956875563, -76.2956862152, 19.4968043268, 19.4968056679}}, + {"etzq7udz", []float64{33.4683036804, 33.4684753418, -12.1361160278, -12.1357727051}}, + {"rf2x5pd6", []float64{-31.0717391968, -31.0715675354, 169.588050842, 169.588394165}}, + {"k8kquxqbt2", []float64{-42.3673152924, -42.3673099279, 28.6838114262, 28.683822155}}, + {"bz91jncb2c7", []float64{87.4004097283, 87.4004110694, -144.621583968, -144.621582627}}, + {"3uk4y5r6sjwr", []float64{-20.5920389481, -20.5920387805, -95.3511917219, -95.3511913866}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"y95n1hd", []float64{51.7044067383, 51.7057800293, 116.765441895, 116.766815186}}, + {"629k5b3kbkc", []float64{-41.4821608365, -41.4821594954, -76.8256638944, -76.8256625533}}, + {"pp42st7vp5", []float64{-50.5073958635, -50.5073904991, 138.367266655, 138.367277384}}, + {"u17e", []float64{52.55859375, 52.734375, 4.921875, 5.2734375}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"ex5w6znubms", []float64{40.5129298568, 40.5129311979, -17.447989583, -17.4479882419}}, + {"8jsn", []float64{31.9921875, 32.16796875, -174.375, -174.0234375}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"mht9efkj", []float64{-19.410610199, -19.4104385376, 52.9046630859, 52.9050064087}}, + {"kkbcqqfcsz", []float64{-18.0241495371, -18.0241441727, 12.5833261013, 12.5833368301}}, + {"866rppwznm", []float64{13.9291459322, 13.9291512966, -165.268782377, -165.268771648}}, + {"96wj53wczp0c", []float64{14.9499841221, 14.9499842897, -115.160106607, -115.160106272}}, + {"9ctzrj", []float64{9.73937988281, 9.74487304688, -92.8564453125, -92.8454589844}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"wfq5hd", []float64{13.1945800781, 13.2000732422, 132.385253906, 132.396240234}}, + {"9y6vu2v8wm5", []float64{36.1712247133, 36.1712260544, -97.1882195771, -97.188218236}}, + {"6xcpg", []float64{-0.0439453125, 0.0, -65.9619140625, -65.91796875}}, + {"rxqgqmc", []float64{-3.61587524414, -3.61450195312, 167.268218994, 167.269592285}}, + {"yye", []float64{81.5625, 82.96875, 127.96875, 129.375}}, + {"r3", []float64{-39.375, -33.75, 146.25, 157.5}}, + {"x7t", []float64{19.6875, 21.09375, 153.28125, 154.6875}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"g9mmh9sku8h", []float64{52.9192113876, 52.9192127287, -14.9133986235, -14.9133972824}}, + {"v2q6qu3", []float64{46.8251037598, 46.8264770508, 65.3370666504, 65.3384399414}}, + {"7j9ckmu00j", []float64{-13.8111609221, -13.8111555576, -42.3468017578, -42.346791029}}, + {"4q3td", []float64{-53.876953125, -53.8330078125, -76.552734375, -76.5087890625}}, + {"9ve2c92z33ke", []float64{31.077454146, 31.0774543136, -96.6126798838, -96.6126795486}}, + {"9sscvm0mw1kw", []float64{25.6485348567, 25.6485350244, -105.58899276, -105.588992424}}, + {"9u", []float64{22.5, 28.125, -101.25, -90.0}}, + {"nv4j7yb8mjjy", []float64{-60.9149988368, -60.9149986692, 126.728203855, 126.728204191}}, + {"80w8", []float64{2.8125, 2.98828125, -170.859375, -170.5078125}}, + {"q06ch78z1", []float64{-43.3975410461, -43.3974981308, 94.0550279617, 94.0550708771}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"gzw", []float64{87.1875, 88.59375, -2.8125, -1.40625}}, + {"1", []float64{-90.0, -45.0, -135.0, -90.0}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"furb", []float64{68.90625, 69.08203125, -45.3515625, -45.0}}, + {"xen8pyc36", []float64{16.9122934341, 16.9123363495, 166.983003616, 166.983046532}}, + {"n1gc29sd", []float64{-79.9279403687, -79.9277687073, 95.3015899658, 95.3019332886}}, + {"cjvu", []float64{78.046875, 78.22265625, -126.9140625, -126.5625}}, + {"w7x53f7fb0", []float64{20.2716207504, 20.2716261148, 111.175804138, 111.175814867}}, + {"hz", []float64{-50.625, -45.0, 33.75, 45.0}}, + {"8tz14", []float64{32.51953125, 32.5634765625, -147.568359375, -147.524414062}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"pyfunm", []float64{-51.3006591797, -51.2951660156, 172.891845703, 172.902832031}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"7xre21ceuxv", []float64{-3.63716259599, -3.63716125488, -11.9508652389, -11.9508638978}}, + {"17", []float64{-73.125, -67.5, -123.75, -112.5}}, + {"ru", []float64{-22.5, -16.875, 168.75, 180.0}}, + {"bdjs6y77m", []float64{57.0319604874, 57.0320034027, -149.640097618, -149.640054703}}, + {"u1vsgx2", []float64{55.718536377, 55.719909668, 7.88818359375, 7.88955688477}}, + {"pj5e80uz", []float64{-61.2544441223, -61.2542724609, 139.928398132, 139.928741455}}, + {"ju01xcg9", []float64{-67.2265434265, -67.2263717651, 79.0953826904, 79.0957260132}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"k05kz1", []float64{-44.1595458984, -44.1540527344, 4.8779296875, 4.88891601562}}, + {"ru2h1xc", []float64{-20.3480529785, -20.3466796875, 168.81729126, 168.818664551}}, + {"ud2s6zr", []float64{58.443145752, 58.444519043, 23.3335876465, 23.3349609375}}, + {"mrq5fm1zrqp", []float64{-3.5308277607, -3.53082641959, 64.7891007364, 64.7891020775}}, + {"0y2u0dg2rg", []float64{-54.1254597902, -54.1254544258, -145.168544054, -145.168533325}}, + {"9nkt0rnyx3", []float64{36.0747295618, 36.0747349262, -128.651307821, -128.651297092}}, + {"77q", []float64{-26.71875, -25.3125, -25.3125, -23.90625}}, + {"ng76t7", []float64{-71.2628173828, -71.2573242188, 128.551025391, 128.562011719}}, + {"4ewypr0y27up", []float64{-69.2182661779, -69.2182660103, -57.6881629229, -57.6881625876}}, + {"ge7vkm2x73", []float64{64.2341905832, 64.2341959476, -17.0389688015, -17.0389580727}}, + {"3qm4d", []float64{-9.404296875, -9.3603515625, -116.630859375, -116.586914062}}, + {"6gqw9", []float64{-25.576171875, -25.5322265625, -47.0654296875, -47.021484375}}, + {"32", []float64{-45.0, -39.375, -123.75, -112.5}}, + {"ns85", []float64{-64.16015625, -63.984375, 112.5, 112.8515625}}, + {"hzy00b4j5tcj", []float64{-46.4053600095, -46.4053598419, 42.2233571112, 42.2233574465}}, + {"7qrk5nt", []float64{-9.10491943359, -9.10354614258, -23.4159851074, -23.4146118164}}, + {"vd4219t7th", []float64{56.2588620186, 56.258867383, 70.7374048233, 70.7374155521}}, + {"g", []float64{45.0, 90.0, -45.0, 0.0}}, + {"pq1p16t", []float64{-55.0057983398, -55.0044250488, 147.718048096, 147.719421387}}, + {"gryfsc3wgn", []float64{89.0412604809, 89.0412658453, -24.0468835831, -24.0468728542}}, + {"np0u", []float64{-49.921875, -49.74609375, 91.0546875, 91.40625}}, + {"u87", []float64{46.40625, 47.8125, 26.71875, 28.125}}, + {"9qz2", []float64{37.96875, 38.14453125, -113.5546875, -113.203125}}, + {"xunf", []float64{22.8515625, 23.02734375, 178.2421875, 178.59375}}, + {"ve", []float64{61.875, 67.5, 67.5, 78.75}}, + {"s2c1tf2bvp4s", []float64{4.49494846165, 4.49494862929, 12.9101834446, 12.9101837799}}, + {"5znd0f", []float64{-50.2624511719, -50.2569580078, -2.07641601562, -2.0654296875}}, + {"dn90ug", []float64{36.7108154297, 36.7163085938, -88.3850097656, -88.3740234375}}, + {"24bg3", []float64{-28.9599609375, -28.916015625, -178.901367188, -178.857421875}}, + {"x46xpb2", []float64{13.888092041, 13.889465332, 138.856201172, 138.857574463}}, + {"83q", []float64{7.03125, 8.4375, -160.3125, -158.90625}}, + {"2pup20sqj", []float64{-0.128059387207, -0.128016471863, -174.368948936, -174.368906021}}, + {"07", []float64{-73.125, -67.5, -168.75, -157.5}}, + {"jj7nh21g4zk", []float64{-59.4135086238, -59.4135072827, 49.408044219, 49.4080455601}}, + {"19up4rw9", []float64{-78.8844108582, -78.8842391968, -106.767196655, -106.766853333}}, + {"2j", []float64{-16.875, -11.25, -180.0, -168.75}}, + {"14uexty", []float64{-73.8844299316, -73.8830566406, -128.33404541, -128.332672119}}, + {"t3wtb", []float64{9.4482421875, 9.4921875, 65.390625, 65.4345703125}}, + {"wv0z", []float64{29.35546875, 29.53125, 124.8046875, 125.15625}}, + {"jj6gcte19u", []float64{-59.7790789604, -59.779073596, 48.9373004436, 48.9373111725}}, + {"xz0wc0te1bbe", []float64{40.5647895299, 40.5647896975, 169.504699185, 169.504699521}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"zyejp1um86c", []float64{82.4519781768, 82.4519795179, 173.282215744, 173.282217085}}, + {"ft915xruxj8n", []float64{76.1539096758, 76.1539098434, -65.9289979935, -65.9289976582}}, + {"vchvx0yp5c", []float64{51.5971237421, 51.5971291065, 85.7457053661, 85.745716095}}, + {"x5x", []float64{19.6875, 21.09375, 144.84375, 146.25}}, + {"0ykju58", []float64{-53.8137817383, -53.8124084473, -140.44921875, -140.447845459}}, + {"d2yk35fd", []float64{4.98676300049, 4.98693466187, -69.91355896, -69.9132156372}}, + {"6ymr7k8", []float64{-8.54461669922, -8.5432434082, -48.7243652344, -48.7229919434}}, + {"pjsxb9f", []float64{-57.6905822754, -57.6892089844, 141.352844238, 141.354217529}}, + {"trydkh27gn2t", []float64{44.0132818557, 44.0132820234, 65.5668789893, 65.5668793246}}, + {"k72c0uqqw2", []float64{-26.5185070038, -26.5185016394, 12.3464977741, 12.346508503}}, + {"s8trjfz", []float64{4.05807495117, 4.05944824219, 30.145111084, 30.146484375}}, + {"57ffkwfnrh1", []float64{-68.4725689888, -68.4725676477, -29.6820102632, -29.6820089221}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"u2k", []float64{46.40625, 47.8125, 16.875, 18.28125}}, + {"nndqt", []float64{-52.294921875, -52.2509765625, 93.3837890625, 93.427734375}}, + {"w7", []float64{16.875, 22.5, 101.25, 112.5}}, + {"r6c7x00p6", []float64{-28.91477108, -28.9147281647, 148.315515518, 148.315558434}}, + {"mdrgz", []float64{-31.6845703125, -31.640625, 78.7060546875, 78.75}}, + {"f1dzhud0j", []float64{54.6926879883, 54.6927309036, -85.9211111069, -85.9210681915}}, + {"yqh", []float64{78.75, 80.15625, 106.875, 108.28125}}, + {"9jp43kj", []float64{28.5424804688, 28.5438537598, -125.094451904, -125.093078613}}, + {"14pb4v5q", []float64{-78.7215042114, -78.72133255, -123.976249695, -123.975906372}}, + {"bjzkjzr9hsvc", []float64{78.0868977495, 78.0868979171, -169.54150144, -169.541501105}}, + {"svjbhs9e9g8", []float64{28.1503388286, 28.1503401697, 42.0358264446, 42.0358277857}}, + {"guuxc8c5v", []float64{73.0858182907, 73.0858612061, -4.85436916351, -4.85432624817}}, + {"utu2603h0ru5", []float64{77.3897973262, 77.3897974938, 28.5658425093, 28.5658428445}}, + {"bq", []float64{78.75, 84.375, -168.75, -157.5}}, + {"kk", []float64{-22.5, -16.875, 11.25, 22.5}}, + {"6vxq65mhx", []float64{-12.9452419281, -12.9451990128, -45.9596300125, -45.9595870972}}, + {"f4sb", []float64{59.0625, 59.23828125, -83.3203125, -82.96875}}, + {"y5p4", []float64{62.2265625, 62.40234375, 99.84375, 100.1953125}}, + {"bs6cju", []float64{69.1040039062, 69.1094970703, -153.380126953, -153.369140625}}, + {"5j", []float64{-61.875, -56.25, -45.0, -33.75}}, + {"4e8z0qpsppx", []float64{-69.048345387, -69.0483440459, -66.4237166941, -66.423715353}}, + {"nbyg", []float64{-85.25390625, -85.078125, 133.2421875, 133.59375}}, + {"8jn2dnxvbd", []float64{28.2495939732, 28.2495993376, -171.112382412, -171.112371683}}, + {"0h6ej9g", []float64{-65.5567932129, -65.5554199219, -176.238555908, -176.237182617}}, + {"9j18njf9mc", []float64{28.1568056345, 28.1568109989, -132.623273134, -132.623262405}}, + {"pf93jtqd2xm9", []float64{-75.7324543409, -75.7324541733, 170.758466944, 170.758467279}}, + {"hc2", []float64{-82.96875, -81.5625, 33.75, 35.15625}}, + {"g", []float64{45.0, 90.0, -45.0, 0.0}}, + {"cewhub", []float64{65.5224609375, 65.5279541016, -103.853759766, -103.842773438}}, + {"2vcrfbgsv2sx", []float64{-11.2890061922, -11.2890060246, -144.366300032, -144.366299696}}, + {"mxdsnv", []float64{-2.08190917969, -2.07641601562, 71.3122558594, 71.3232421875}}, + {"03", []float64{-84.375, -78.75, -168.75, -157.5}}, + {"u73kybuxbq", []float64{64.1216933727, 64.1216987371, 13.3106338978, 13.3106446266}}, + {"uz2", []float64{85.78125, 87.1875, 33.75, 35.15625}}, + {"3w", []float64{-11.25, -5.625, -112.5, -101.25}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"q8kttcg9t", []float64{-42.6170825958, -42.6170396805, 119.085831642, 119.085874557}}, + {"j8w8vhmh9d", []float64{-87.0315349102, -87.0315295458, 76.8672823906, 76.8672931194}}, + {"qxn54fn91g", []float64{-5.08648216724, -5.08647680283, 121.067351103, 121.067361832}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"3h2p4vmu", []float64{-19.8337554932, -19.8335838318, -134.871253967, -134.870910645}}, + {"j4re6xcw", []float64{-76.7288589478, -76.7286872864, 55.6587982178, 55.6591415405}}, + {"ugkmrc1vj", []float64{64.2104530334, 64.2104959488, 40.0697565079, 40.0697994232}}, + {"n1mj98dq", []float64{-81.9981765747, -81.9980049133, 97.1002578735, 97.1006011963}}, + {"r6c", []float64{-29.53125, -28.125, 147.65625, 149.0625}}, + {"9uksmr", []float64{24.6917724609, 24.697265625, -94.6911621094, -94.6801757812}}, + {"dmve", []float64{32.87109375, 33.046875, -71.015625, -70.6640625}}, + {"jgrdkvuq", []float64{-71.2906265259, -71.2904548645, 89.5114517212, 89.5117950439}}, + {"94g", []float64{15.46875, 16.875, -130.78125, -129.375}}, + {"2vj4gt", []float64{-16.3641357422, -16.3586425781, -139.064941406, -139.053955078}}, + {"q39q2pm5kxt", []float64{-35.4234436154, -35.4234422743, 103.01487878, 103.014880121}}, + {"qcuy493hpwdf", []float64{-34.0939741954, -34.0939740278, 130.541249625, 130.541249961}}, + {"nhfpjqpyqnv3", []float64{-62.0167130046, -62.0167128369, 93.0541204289, 93.0541207641}}, + {"838kk", []float64{9.1845703125, 9.228515625, -168.22265625, -168.178710938}}, + {"zx2fdg", []float64{86.2371826172, 86.2426757812, 158.675537109, 158.686523438}}, + {"j7ktd1g4", []float64{-70.7419967651, -70.7418251038, 62.670135498, 62.6704788208}}, + {"yzp", []float64{84.375, 85.78125, 133.59375, 135.0}}, + {"76kf7tcsnb94", []float64{-31.9159668311, -31.9159666635, -26.91415295, -26.9141526148}}, + {"9jb", []float64{32.34375, 33.75, -135.0, -133.59375}}, + {"6w", []float64{-11.25, -5.625, -67.5, -56.25}}, + {"f2zs58", []float64{49.921875, 49.9273681641, -68.0493164062, -68.0383300781}}, + {"f0", []float64{45.0, 50.625, -90.0, -78.75}}, + {"mnqum74bk", []float64{-9.08015727997, -9.08011436462, 54.7268486023, 54.7268915176}}, + {"t6rhggbn5", []float64{13.512840271, 13.5128831863, 66.2586736679, 66.2587165833}}, + {"g9q", []float64{52.03125, 53.4375, -14.0625, -12.65625}}, + {"7vr", []float64{-15.46875, -14.0625, -1.40625, 0.0}}, + {"t6sr47h", []float64{15.3094482422, 15.3108215332, 62.3309326172, 62.3323059082}}, + {"076vzrw", []float64{-70.666809082, -70.665435791, -164.555969238, -164.554595947}}, + {"s2", []float64{0.0, 5.625, 11.25, 22.5}}, + {"gd7350xxrrs", []float64{57.8360626101, 57.8360639513, -17.7872353792, -17.7872340381}}, + {"0p8sgbg", []float64{-46.9734191895, -46.9720458984, -179.127960205, -179.126586914}}, + {"5zsn39", []float64{-46.7083740234, -46.7028808594, -5.55908203125, -5.54809570312}}, + {"80f", []float64{4.21875, 5.625, -177.1875, -175.78125}}, + {"cymr4xm05c0", []float64{81.4265495539, 81.426550895, -93.7502968311, -93.75029549}}, + {"qmjmz", []float64{-15.8642578125, -15.8203125, 108.940429688, 108.984375}}, + {"39c0", []float64{-35.15625, -34.98046875, -111.09375, -110.7421875}}, + {"pgxnue0jpfn", []float64{-69.1086280346, -69.1086266935, 178.791844547, 178.791845888}}, + {"nytxjp", []float64{-52.1685791016, -52.1630859375, 131.704101562, 131.715087891}}, + {"q1mvpgze", []float64{-37.0687294006, -37.0685577393, 98.4368133545, 98.4371566772}}, + {"tqjgn4h", []float64{34.2883300781, 34.2897033691, 64.6051025391, 64.6064758301}}, + {"tjn18qrtdk", []float64{28.4239697456, 28.4239751101, 53.4588825703, 53.4588932991}}, + {"qq3w12r", []float64{-8.78768920898, -8.78631591797, 103.423919678, 103.425292969}}, + {"zzdu8g6", []float64{87.9963684082, 87.9977416992, 172.652893066, 172.654266357}}, + {"mz72xeccms3t", []float64{-4.11002179608, -4.11002162844, 83.6525436491, 83.6525439844}}, + {"s7fnw5nzhbs4", []float64{22.2540122643, 22.2540124319, 14.3356508017, 14.3356511369}}, + {"76rtxb6nkdm", []float64{-31.3744948804, -31.3744935393, -22.8596024215, -22.8596010804}}, + {"znbejer93dr", []float64{83.5141731799, 83.514174521, 135.955197662, 135.955199003}}, + {"smk7u7bz27", []float64{30.212289691, 30.2122950554, 17.4143707752, 17.4143815041}}, + {"yvmurs", []float64{75.3002929688, 75.3057861328, 132.165527344, 132.176513672}}, + {"tnjn6dgd8", []float64{34.8641681671, 34.8642110825, 52.1459197998, 52.1459627151}}, + {"gyee1hnu", []float64{82.1125030518, 82.1126747131, -6.27490997314, -6.27456665039}}, + {"gjwh9n02wfmc", []float64{76.7615726776, 76.7615728453, -36.5179139748, -36.5179136395}}, + {"n6jh51hrnfws", []float64{-78.0401661247, -78.0401659571, 108.41922082, 108.419221155}}, + {"shgszu46pudj", []float64{27.5760518946, 27.5760520622, 5.26587635279, 5.26587668806}}, + {"3c", []float64{-39.375, -33.75, -101.25, -90.0}}, + {"fy", []float64{78.75, 84.375, -56.25, -45.0}}, + {"s75d5tn3m", []float64{17.254242897, 17.2542858124, 16.3344812393, 16.3345241547}}, + {"2c1c9kkw8dk5", []float64{-39.0868538059, -39.0868536383, -143.727924228, -143.727923892}}, + {"5yugurey8e2", []float64{-51.3297383487, -51.3297370076, -4.37837362289, -4.37837228179}}, + {"rd", []float64{-33.75, -28.125, 157.5, 168.75}}, + {"um", []float64{73.125, 78.75, 11.25, 22.5}}, + {"bkgc", []float64{71.89453125, 72.0703125, -163.4765625, -163.125}}, + {"8cfxnrphhu0", []float64{11.1133790016, 11.1133803427, -142.449899912, -142.449898571}}, + {"tdm", []float64{12.65625, 14.0625, 74.53125, 75.9375}}, + {"y9usehucn02q", []float64{55.6610321626, 55.6610323302, 118.966741897, 118.966742232}}, + {"vk7kbfk0vf33", []float64{69.7537115403, 69.7537117079, 60.859013088, 60.8590134233}}, + {"ewyx82", []float64{39.287109375, 39.2926025391, -13.3483886719, -13.3374023438}}, + {"ev8c3", []float64{31.1572265625, 31.201171875, -10.1513671875, -10.107421875}}, + {"37p4fhuc", []float64{-27.6153373718, -27.6151657104, -113.811836243, -113.81149292}}, + {"0yvh2p9wbu", []float64{-51.2418007851, -51.2417954206, -139.216657877, -139.216647148}}, + {"81k", []float64{7.03125, 8.4375, -174.375, -172.96875}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"5c67rs", []float64{-82.3754882812, -82.3699951172, -7.75634765625, -7.74536132812}}, + {"udjvtg", []float64{57.2332763672, 57.2387695312, 30.8386230469, 30.849609375}}, + {"8b56vfqrppu", []float64{0.497001260519, 0.497002601624, -141.418113112, -141.418111771}}, + {"xv50", []float64{28.125, 28.30078125, 172.96875, 173.3203125}}, + {"7ep", []float64{-28.125, -26.71875, -12.65625, -11.25}}, + {"bxzp4746", []float64{89.8410415649, 89.8412132263, -147.554283142, -147.553939819}}, + {"5d54r", []float64{-78.3544921875, -78.310546875, -17.9736328125, -17.9296875}}, + {"hhknsytff", []float64{-64.9149942398, -64.9149513245, 5.8417224884, 5.84176540375}}, + {"gvjjq75", []float64{74.0643310547, 74.0657043457, -3.93997192383, -3.93859863281}}, + {"6ryrt5", []float64{-0.0714111328125, -0.06591796875, -69.7412109375, -69.7302246094}}, + {"tykj1vq1gj", []float64{36.0643225908, 36.0643279552, 84.460272789, 84.4602835178}}, + {"20tdw84b1w", []float64{-41.7480146885, -41.7480093241, -171.976139545, -171.976128817}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"sbwdq1c4355", []float64{3.21802318096, 3.21802452207, 43.1557171047, 43.1557184458}}, + {"rwhkzg", []float64{-10.3985595703, -10.3930664062, 163.817138672, 163.828125}}, + {"wrzphwjw57", []float64{44.8582237959, 44.8582291603, 111.299196482, 111.299207211}}, + {"674", []float64{-28.125, -26.71875, -75.9375, -74.53125}}, + {"z8kb", []float64{46.40625, 46.58203125, 164.1796875, 164.53125}}, + {"pmudq9vs33", []float64{-57.2503942251, -57.2503888607, 152.871376276, 152.871387005}}, + {"j1br4n9", []float64{-78.8900756836, -78.8887023926, 45.440826416, 45.442199707}}, + {"ccc", []float64{54.84375, 56.25, -99.84375, -98.4375}}, + {"src", []float64{43.59375, 45.0, 12.65625, 14.0625}}, + {"cc51keq", []float64{50.8625793457, 50.8639526367, -96.8252563477, -96.8238830566}}, + {"pr", []float64{-50.625, -45.0, 146.25, 157.5}}, + {"tvd9", []float64{31.11328125, 31.2890625, 82.265625, 82.6171875}}, + {"489bdms44x", []float64{-87.069016099, -87.0690107346, -64.9345850945, -64.9345743656}}, + {"cmn23svsyv", []float64{73.1958800554, 73.1958854198, -114.887176752, -114.887166023}}, + {"dm9vug1194", []float64{31.9649899006, 31.964995265, -76.0789060593, -76.0788953304}}, + {"45f7", []float64{-68.37890625, -68.203125, -86.8359375, -86.484375}}, + {"mxuxkdtgy50", []float64{-0.117443203926, -0.117441862822, 74.0340328217, 74.0340341628}}, + {"tdwd7", []float64{14.4580078125, 14.501953125, 76.7724609375, 76.81640625}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"jss4y", []float64{-64.2041015625, -64.16015625, 73.388671875, 73.4326171875}}, + {"tmcs", []float64{33.046875, 33.22265625, 58.359375, 58.7109375}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"gc61ncnxuec", []float64{52.2138749063, 52.2138762474, -8.13174828887, -8.13174694777}}, + {"jwpc", []float64{-56.07421875, -55.8984375, 78.3984375, 78.75}}, + {"yq5gn", []float64{79.27734375, 79.3212890625, 106.787109375, 106.831054688}}, + {"uhtmex", []float64{71.3177490234, 71.3232421875, 7.53662109375, 7.54760742188}}, + {"n0hbvyc", []float64{-89.8310852051, -89.8297119141, 96.9337463379, 96.9351196289}}, + {"kp8q0gk0h", []float64{-1.7399597168, -1.73991680145, 0.390186309814, 0.390229225159}}, + {"yjzduj46p", []float64{77.8549575806, 77.8550004959, 100.726046562, 100.726089478}}, + {"8hhkjyy4v5s", []float64{23.2406947017, 23.2406960428, -173.762292266, -173.762290925}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"wwu63", []float64{38.3642578125, 38.408203125, 118.520507812, 118.564453125}}, + {"z9rnym", []float64{53.2452392578, 53.2507324219, 167.618408203, 167.629394531}}, + {"78fnfc", []float64{-39.5892333984, -39.5837402344, -19.5666503906, -19.5556640625}}, + {"8dc1mqyer", []float64{15.7261133194, 15.7261562347, -155.85381031, -155.853767395}}, + {"b5", []float64{61.875, 67.5, -180.0, -168.75}}, + {"q3zq3gz6w7", []float64{-34.0365725756, -34.0365672112, 111.532441378, 111.532452106}}, + {"xx6", []float64{40.78125, 42.1875, 160.3125, 161.71875}}, + {"r3", []float64{-39.375, -33.75, 146.25, 157.5}}, + {"dytz0swq74e", []float64{37.8187742829, 37.818775624, -48.1333740056, -48.1333726645}}, + {"gpwu", []float64{87.890625, 88.06640625, -35.5078125, -35.15625}}, + {"9ywdf6cr2w7", []float64{37.0622827113, 37.0622840524, -92.0087559521, -92.008754611}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"ues", []float64{64.6875, 66.09375, 28.125, 29.53125}}, + {"qggvs5q7pr", []float64{-22.9210478067, -22.9210424423, 129.208112955, 129.208123684}}, + {"fy9d8y2mv2", []float64{82.0372724533, 82.0372778177, -54.1070973873, -54.1070866585}}, + {"bxx92bb60s", []float64{87.411711216, 87.4117165804, -146.919801235, -146.919790506}}, + {"uugkmp4jkm0", []float64{72.5052005053, 72.5052018464, 38.5429680347, 38.5429693758}}, + {"7mmd13sd30", []float64{-15.1085615158, -15.1085561514, -25.9544706345, -25.9544599056}}, + {"5nn2", []float64{-56.25, -56.07421875, -36.2109375, -35.859375}}, + {"jf9sz2r", []float64{-75.1011657715, -75.0997924805, 81.1875915527, 81.1889648438}}, + {"3r", []float64{-5.625, 0.0, -123.75, -112.5}}, + {"yw", []float64{78.75, 84.375, 112.5, 123.75}}, + {"yt31y5hwc3c5", []float64{74.8565152846, 74.8565154523, 114.17615667, 114.176157005}}, + {"7vgv9xhmn", []float64{-11.6501426697, -11.6500997543, -5.90455055237, -5.90450763702}}, + {"f10tgm4q6", []float64{51.6642808914, 51.6643238068, -89.1508769989, -89.1508340836}}, + {"hepj84tfcj", []float64{-72.143971324, -72.1439659595, 32.3516893387, 32.3517000675}}, + {"zg", []float64{61.875, 67.5, 168.75, 180.0}}, + {"by12", []float64{78.75, 78.92578125, -144.4921875, -144.140625}}, + {"51", []float64{-84.375, -78.75, -45.0, -33.75}}, + {"w44s78ur", []float64{12.0023918152, 12.0025634766, 93.6752700806, 93.6756134033}}, + {"2tcr0", []float64{-11.42578125, -11.3818359375, -155.7421875, -155.698242188}}, + {"p0n", []float64{-90.0, -88.59375, 143.4375, 144.84375}}, + {"u1", []float64{50.625, 56.25, 0.0, 11.25}}, + {"nygu1mshqxku", []float64{-51.2971434742, -51.2971433066, 129.084147625, 129.08414796}}, + {"3khrs6gty5", []float64{-21.1655312777, -21.1655259132, -117.581605911, -117.581595182}}, + {"4n", []float64{-56.25, -50.625, -90.0, -78.75}}, + {"tj8pjxb5", []float64{32.2110557556, 32.211227417, 45.2416992188, 45.2420425415}}, + {"7nhpg3stdjgu", []float64{-9.87847991288, -9.87847974524, -39.225907065, -39.2259067297}}, + {"rhtcg24t2s50", []float64{-19.3789601326, -19.378959965, 143.232218474, 143.232218809}}, + {"5vx7c75x", []float64{-58.3856391907, -58.3854675293, -0.99494934082, -0.994606018066}}, + {"cs4kgc65z", []float64{68.3424711227, 68.3425140381, -109.168095589, -109.168052673}}, + {"k3sn2mb", []float64{-35.4322814941, -35.4309082031, 16.8859863281, 16.8873596191}}, + {"ud7s8v", []float64{58.4747314453, 58.4802246094, 27.4548339844, 27.4658203125}}, + {"8qqg2ue1mr6b", []float64{35.7525117695, 35.7525119372, -159.220504649, -159.220504314}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"kwtg55d3me", []float64{-7.89069950581, -7.89069414139, 30.7210993767, 30.7211101055}}, + {"mqsedtkq3", []float64{-7.79235363007, -7.79231071472, 62.6938676834, 62.6939105988}}, + {"94j6tg", []float64{11.7059326172, 11.7114257812, -127.364501953, -127.353515625}}, + {"wd45s", []float64{11.865234375, 11.9091796875, 115.48828125, 115.532226562}}, + {"fwgxjq0n2", []float64{84.233250618, 84.2332935333, -62.3474121094, -62.347369194}}, + {"1k8fh1", []float64{-64.3304443359, -64.3249511719, -122.51953125, -122.508544922}}, + {"h", []float64{-90.0, -45.0, 0.0, 45.0}}, + {"f", []float64{45.0, 90.0, -90.0, -45.0}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"qh", []float64{-22.5, -16.875, 90.0, 101.25}}, + {"24nszw5", []float64{-32.8820800781, -32.8807067871, -170.525665283, -170.524291992}}, + {"p8rfy", []float64{-88.1103515625, -88.06640625, 168.662109375, 168.706054688}}, + {"st23m", []float64{29.7509765625, 29.794921875, 23.0712890625, 23.115234375}}, + {"zgg3", []float64{66.26953125, 66.4453125, 173.3203125, 173.671875}}, + {"zgsgk7bcz8k", []float64{65.2796901762, 65.2796915174, 175.617812276, 175.617813617}}, + {"js0", []float64{-67.5, -66.09375, 67.5, 68.90625}}, + {"9zs3bh", []float64{42.5170898438, 42.5225830078, -95.2734375, -95.2624511719}}, + {"k8qd0d6mqfz", []float64{-43.2289119065, -43.2289105654, 31.6659866273, 31.6659879684}}, + {"xrj", []float64{39.375, 40.78125, 153.28125, 154.6875}}, + {"0mbxv2r4", []float64{-56.2922286987, -56.2920570374, -167.806549072, -167.80620575}}, + {"bdf8wz8g1", []float64{60.5983543396, 60.5983972549, -153.686671257, -153.686628342}}, + {"emgeh3mkyu", []float64{32.8787970543, 32.8788024187, -28.6338579655, -28.6338472366}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"156p3nxfwq", []float64{-70.4081690311, -70.4081636667, -132.132643461, -132.132632732}}, + {"stduvpcnp4", []float64{31.8160736561, 31.8160790205, 26.5885877609, 26.5885984898}}, + {"shk8evrmmbgr", []float64{24.0238861553, 24.023886323, 6.50312740356, 6.50312773883}}, + {"7ynw", []float64{-10.1953125, -10.01953125, -2.109375, -1.7578125}}, + {"r4zgvkp", []float64{-28.8500976562, -28.8487243652, 146.138763428, 146.140136719}}, + {"5b9p", []float64{-85.95703125, -85.78125, -9.84375, -9.4921875}}, + {"gsp", []float64{67.5, 68.90625, -12.65625, -11.25}}, + {"ek4mmvr", []float64{23.4516906738, 23.4530639648, -30.323638916, -30.322265625}}, + {"hd2nu", []float64{-76.1572265625, -76.11328125, 22.67578125, 22.7197265625}}, + {"xzp0t", []float64{39.462890625, 39.5068359375, 178.813476562, 178.857421875}}, + {"fycjx9", []float64{83.9410400391, 83.9465332031, -54.5141601562, -54.5031738281}}, + {"x4ygy62x5", []float64{16.1414909363, 16.1415338516, 144.767661095, 144.76770401}}, + {"37", []float64{-28.125, -22.5, -123.75, -112.5}}, + {"m40", []float64{-33.75, -32.34375, 45.0, 46.40625}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"b5", []float64{61.875, 67.5, -180.0, -168.75}}, + {"zwd", []float64{81.5625, 82.96875, 160.3125, 161.71875}}, + {"qgnb2g", []float64{-28.0645751953, -28.0590820312, 133.275146484, 133.286132812}}, + {"7w", []float64{-11.25, -5.625, -22.5, -11.25}}, + {"v6x", []float64{59.0625, 60.46875, 66.09375, 67.5}}, + {"018v8j6y", []float64{-80.5658340454, -80.565662384, -178.94153595, -178.941192627}}, + {"mm4p02h18xc", []float64{-15.6442321837, -15.6442308426, 59.079002291, 59.0790036321}}, + {"6cty1v7", []float64{-35.4789733887, -35.4776000977, -48.0830383301, -48.0816650391}}, + {"g6rzs", []float64{58.974609375, 59.0185546875, -22.67578125, -22.6318359375}}, + {"58qb", []float64{-88.59375, -88.41796875, -13.0078125, -12.65625}}, + {"n8v", []float64{-85.78125, -84.375, 119.53125, 120.9375}}, + {"h1nj4b1uv", []float64{-83.4952783585, -83.4952354431, 8.56096744537, 8.56101036072}}, + {"qt4ukphd", []float64{-16.0891342163, -16.0889625549, 116.54914856, 116.549491882}}, + {"n2", []float64{-90.0, -84.375, 101.25, 112.5}}, + {"50sux01hejpj", []float64{-86.3956842385, -86.3956840709, -38.0111838877, -38.0111835524}}, + {"4exy", []float64{-69.2578125, -69.08203125, -56.6015625, -56.25}}, + {"x9t8r", []float64{8.4814453125, 8.525390625, 165.541992188, 165.5859375}}, + {"785m2wrxgw2", []float64{-44.0414522588, -44.0414509177, -17.8972649574, -17.8972636163}}, + {"0exegdddqf", []float64{-69.6391904354, -69.639185071, -146.7955935, -146.795582771}}, + {"ynrw", []float64{81.2109375, 81.38671875, 100.546875, 100.8984375}}, + {"uqdmuzrz6", []float64{82.6143121719, 82.6143550873, 14.6335315704, 14.6335744858}}, + {"gchp3yu15c", []float64{51.9366699457, 51.9366753101, -5.54244160652, -5.54243087769}}, + {"415qzwpj2r", []float64{-83.154578805, -83.1545734406, -85.0904738903, -85.0904631615}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"vzq07z4", []float64{85.8636474609, 85.865020752, 87.3550415039, 87.3564147949}}, + {"w43s", []float64{13.359375, 13.53515625, 92.109375, 92.4609375}}, + {"b7td", []float64{65.0390625, 65.21484375, -161.015625, -160.6640625}}, + {"2fpe", []float64{-33.22265625, -33.046875, -135.703125, -135.3515625}}, + {"nyt", []float64{-53.4375, -52.03125, 130.78125, 132.1875}}, + {"g", []float64{45.0, 90.0, -45.0, 0.0}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"dkzk", []float64{27.421875, 27.59765625, -68.5546875, -68.203125}}, + {"r3925m207w", []float64{-36.5335857868, -36.5335804224, 148.150784969, 148.150795698}}, + {"0zy5gndwbzt3", []float64{-45.710165631, -45.7101654634, -137.677191608, -137.677191272}}, + {"46ex1dmu9", []float64{-74.6938991547, -74.6938562393, -73.7542676926, -73.7542247772}}, + {"jf74h9rrvnz8", []float64{-76.9839544594, -76.9839542918, 83.1766849011, 83.1766852364}}, + {"6vj1jtgv", []float64{-16.6667747498, -16.6666030884, -48.9719009399, -48.9715576172}}, + {"ntsx", []float64{-57.83203125, -57.65625, 118.828125, 119.1796875}}, + {"ehr3ejb", []float64{24.2015075684, 24.2028808594, -34.6728515625, -34.6714782715}}, + {"p7", []float64{-73.125, -67.5, 146.25, 157.5}}, + {"re7", []float64{-26.71875, -25.3125, 161.71875, 163.125}}, + {"66x6j7sc0t", []float64{-30.5665129423, -30.5665075779, -68.3174300194, -68.3174192905}}, + {"mywcjb2q", []float64{-8.25931549072, -8.25914382935, 88.4952163696, 88.4955596924}}, + {"f88jrw", []float64{48.7683105469, 48.7738037109, -67.1704101562, -67.1594238281}}, + {"bjty3ef9n3y6", []float64{77.0569135621, 77.0569137298, -171.844434701, -171.844434366}}, + {"jhz66rh4fj7s", []float64{-62.8467891365, -62.8467889689, 55.2997731417, 55.299773477}}, + {"u16r3nm", []float64{53.3399963379, 53.3413696289, 3.21487426758, 3.21624755859}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"q5rwnj4", []float64{-25.6365966797, -25.6352233887, 100.813293457, 100.814666748}}, + {"dsqd4vc85c", []float64{24.2894035578, 24.2894089222, -58.2363045216, -58.2362937927}}, + {"bzpu7z2qxf9", []float64{85.1630249619, 85.1630263031, -135.18609032, -135.186088979}}, + {"e805cvqt", []float64{0.688877105713, 0.68904876709, -22.4141693115, -22.4138259888}}, + {"y9su01vg", []float64{54.1507530212, 54.1509246826, 119.187583923, 119.187927246}}, + {"41", []float64{-84.375, -78.75, -90.0, -78.75}}, + {"vu3f", []float64{69.2578125, 69.43359375, 81.2109375, 81.5625}}, + {"86", []float64{11.25, 16.875, -168.75, -157.5}}, + {"wpuksnn65", []float64{44.4180679321, 44.4181108475, 96.1610555649, 96.1610984802}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"nrqk483fq0kp", []float64{-48.5138629563, -48.5138627887, 110.151591897, 110.151592232}}, + {"cg0mc", []float64{62.8857421875, 62.9296875, -100.854492188, -100.810546875}}, + {"myt75g", []float64{-7.89367675781, -7.88818359375, 86.2976074219, 86.30859375}}, + {"ugxe27b", []float64{65.2793884277, 65.2807617188, 44.3078613281, 44.3092346191}}, + {"wd845dtbhs8", []float64{14.42781955, 14.4278208911, 112.661898136, 112.661899477}}, + {"c8ef9h6mr", []float64{48.2762002945, 48.2762432098, -107.179226875, -107.17918396}}, + {"bx", []float64{84.375, 90.0, -157.5, -146.25}}, + {"qduv3", []float64{-28.6083984375, -28.564453125, 119.223632812, 119.267578125}}, + {"j86depxm", []float64{-88.1122398376, -88.1120681763, 71.1574172974, 71.1577606201}}, + {"4semjs5hr9p", []float64{-63.7858861685, -63.7858848274, -62.6835371554, -62.6835358143}}, + {"ee", []float64{16.875, 22.5, -22.5, -11.25}}, + {"jxv25m96n", []float64{-46.3756942749, -46.3756513596, 75.0276088715, 75.0276517868}}, + {"vx7gj0006xwe", []float64{86.3086774014, 86.308677569, 72.993280068, 72.9932804033}}, + {"6c", []float64{-39.375, -33.75, -56.25, -45.0}}, + {"ukstfgt78f", []float64{71.3430798054, 71.3430851698, 17.7062165737, 17.7062273026}}, + {"g1h0ye", []float64{50.7733154297, 50.7788085938, -39.0893554688, -39.0783691406}}, + {"5s3j5er", []float64{-65.1969909668, -65.1956176758, -20.9303283691, -20.9289550781}}, + {"6yrnnwq1", []float64{-8.75455856323, -8.75438690186, -46.1123657227, -46.1120223999}}, + {"20fjy", []float64{-39.7705078125, -39.7265625, -176.923828125, -176.879882812}}, + {"1tt417q", []float64{-58.6930847168, -58.6917114258, -105.405578613, -105.404205322}}, + {"hh68", []float64{-66.09375, -65.91796875, 3.515625, 3.8671875}}, + {"85s823r7j", []float64{19.7388267517, 19.7388696671, -173.650717735, -173.65067482}}, + {"3u6n", []float64{-20.0390625, -19.86328125, -98.4375, -98.0859375}}, + {"7w5y8", []float64{-10.107421875, -10.0634765625, -17.2265625, -17.1826171875}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"zk8j", []float64{71.19140625, 71.3671875, 146.25, 146.6015625}}, + {"mgm1evw0", []float64{-26.4248657227, -26.4246940613, 85.954284668, 85.9546279907}}, + {"m7nzv", []float64{-26.7626953125, -26.71875, 65.9619140625, 66.005859375}}, + {"ev89re48", []float64{31.1737060547, 31.1738777161, -10.2138519287, -10.213508606}}, + {"dnc7mrxvb3", []float64{38.5822302103, 38.5822355747, -88.0008208752, -88.0008101463}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"wbksmz281", []float64{2.19314575195, 2.1931886673, 130.331540108, 130.331583023}}, + {"0x3zqt", []float64{-47.9168701172, -47.9113769531, -154.753417969, -154.742431641}}, + {"h97q", []float64{-81.9140625, -81.73828125, 27.0703125, 27.421875}}, + {"ypjt38wtc", []float64{85.3015851974, 85.3016281128, 97.8092622757, 97.809305191}}, + {"d3ey054b7p", []float64{9.50874745846, 9.50875282288, -73.4726572037, -73.4726464748}}, + {"zpbkps3qd9r", []float64{89.3213434517, 89.3213447928, 135.682985634, 135.682986975}}, + {"sqxhhhs68mxy", []float64{37.2908039019, 37.2908040695, 21.2753888592, 21.2753891945}}, + {"293cyj4g6q", []float64{-37.6330769062, -37.6330715418, -154.771517515, -154.771506786}}, + {"w3", []float64{5.625, 11.25, 101.25, 112.5}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"q69s", []float64{-30.234375, -30.05859375, 103.359375, 103.7109375}}, + {"fy1qerq2ven", []float64{79.9325484037, 79.9325497448, -54.3405380845, -54.3405367434}}, + {"62", []float64{-45.0, -39.375, -78.75, -67.5}}, + {"yke1jyek0fg", []float64{70.5246882141, 70.5246895552, 105.725934952, 105.725936294}}, + {"2rcbq1z5c", []float64{-1.35204792023, -1.35200500488, -166.015734673, -166.015691757}}, + {"gue", []float64{70.3125, 71.71875, -7.03125, -5.625}}, + {"t8kqv", []float64{2.5927734375, 2.63671875, 73.6962890625, 73.740234375}}, + {"bgtecc1b", []float64{65.3521728516, 65.3523445129, -138.436317444, -138.435974121}}, + {"8s550", []float64{23.02734375, 23.0712890625, -153.28125, -153.237304688}}, + {"j655kpm1w0b", []float64{-78.1386239827, -78.1386226416, 60.6516551971, 60.6516565382}}, + {"980h", []float64{0.703125, 0.87890625, -112.5, -112.1484375}}, + {"ssywj", []float64{27.7734375, 27.8173828125, 31.8603515625, 31.904296875}}, + {"hrvu", []float64{-45.703125, -45.52734375, 19.3359375, 19.6875}}, + {"3ftuv", []float64{-30.1025390625, -30.05859375, -92.9443359375, -92.900390625}}, + {"zcphg93jux", []float64{51.4678519964, 51.4678573608, 178.749125004, 178.749135733}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"vjxk", []float64{76.640625, 76.81640625, 55.1953125, 55.546875}}, + {"t6cgynpnh4", []float64{16.161929369, 16.1619347334, 58.9843940735, 58.9844048023}}, + {"jspc", []float64{-67.32421875, -67.1484375, 78.3984375, 78.75}}, + {"6w7k2v", []float64{-9.06921386719, -9.06372070312, -62.8967285156, -62.8857421875}}, + {"n6z4", []float64{-74.1796875, -74.00390625, 111.09375, 111.4453125}}, + {"y507hx0", []float64{62.4407958984, 62.4421691895, 90.5493164062, 90.5506896973}}, + {"z17p35rnsc6v", []float64{53.3246401884, 53.324640356, 139.272515886, 139.272516221}}, + {"p8uj5760", []float64{-84.8844909668, -84.8843193054, 163.270568848, 163.27091217}}, + {"8pcszdrxwp", []float64{44.4423955679, 44.4424009323, -177.550477982, -177.550467253}}, + {"mckjc7q4r", []float64{-36.9397687912, -36.9397258759, 84.4384717941, 84.4385147095}}, + {"p6sngm70", []float64{-74.7221374512, -74.7219657898, 152.021942139, 152.022285461}}, + {"59gcptbk", []float64{-79.9481964111, -79.9480247498, -16.8966293335, -16.8962860107}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"fmnv3ug5m", []float64{74.0745019913, 74.0745449066, -69.1765737534, -69.176530838}}, + {"qc8kkbc0xev", []float64{-35.8112038672, -35.8112025261, 124.312004596, 124.312005937}}, + {"b4", []float64{56.25, 61.875, -180.0, -168.75}}, + {"vkxeh", []float64{70.83984375, 70.8837890625, 66.97265625, 67.0166015625}}, + {"399qcn2zv", []float64{-35.3403139114, -35.3402709961, -110.696997643, -110.696954727}}, + {"ybz3", []float64{49.39453125, 49.5703125, 133.9453125, 134.296875}}, + {"d5e25", []float64{19.6875, 19.7314453125, -85.2978515625, -85.25390625}}, + {"5n", []float64{-56.25, -50.625, -45.0, -33.75}}, + {"wdw", []float64{14.0625, 15.46875, 120.9375, 122.34375}}, + {"29q7", []float64{-37.44140625, -37.265625, -148.7109375, -148.359375}}, + {"tqe1y4trw", []float64{36.885137558, 36.8851804733, 60.7398891449, 60.7399320602}}, + {"zfwy172d", []float64{60.135383606, 60.1355552673, 178.297805786, 178.298149109}}, + {"tfd57f", []float64{14.6447753906, 14.6502685547, 81.7272949219, 81.73828125}}, + {"27f6s6h", []float64{-23.4558105469, -23.4544372559, -165.393676758, -165.392303467}}, + {"zk2q2ph7db", []float64{70.0439357758, 70.0439411402, 146.607517004, 146.607527733}}, + {"ptp", []float64{-61.875, -60.46875, 167.34375, 168.75}}, + {"7pcgp042wz", []float64{-0.878782868385, -0.878777503967, -42.2280657291, -42.2280550003}}, + {"9t", []float64{28.125, 33.75, -112.5, -101.25}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"qd0pwby4y", []float64{-32.4270486832, -32.4270057678, 112.805128098, 112.805171013}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"yet", []float64{64.6875, 66.09375, 119.53125, 120.9375}}, + {"v4pbsh7cp", []float64{56.3614082336, 56.361451149, 56.0796689987, 56.0797119141}}, + {"kvr7u7pm7jeu", []float64{-14.7921594232, -14.7921592556, 44.1421702132, 44.1421705484}}, + {"687jj", []float64{-42.71484375, -42.6708984375, -63.0615234375, -63.017578125}}, + {"4w29", []float64{-54.66796875, -54.4921875, -66.796875, -66.4453125}}, + {"6bz45dve", []float64{-40.4140663147, -40.4138946533, -46.2448883057, -46.2445449829}}, + {"gfysykk0nw29", []float64{61.32709058, 61.3270907477, -1.82894401252, -1.82894367725}}, + {"pdw4scw", []float64{-75.4898071289, -75.4884338379, 166.15447998, 166.155853271}}, + {"65f4", []float64{-23.5546875, -23.37890625, -87.1875, -86.8359375}}, + {"jc9g6tpd08j", []float64{-80.9634017944, -80.9634004533, 81.3311286271, 81.3311299682}}, + {"ckw1tvf18r09", []float64{70.608052779, 70.6080529466, -115.057056472, -115.057056136}}, + {"2cbtp", []float64{-34.27734375, -34.2333984375, -145.239257812, -145.1953125}}, + {"54myf", []float64{-76.1572265625, -76.11328125, -36.826171875, -36.7822265625}}, + {"kw", []float64{-11.25, -5.625, 22.5, 33.75}}, + {"mw", []float64{-11.25, -5.625, 67.5, 78.75}}, + {"2bjvee8sgek", []float64{-44.0131442249, -44.0131428838, -138.009411693, -138.009410352}}, + {"yj6r4eyxuv1", []float64{75.783675313, 75.7836766541, 93.2830573618, 93.2830587029}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"pkec4ng", []float64{-64.4746398926, -64.4732666016, 151.615447998, 151.616821289}}, + {"uvpynzd0v", []float64{74.2210149765, 74.2210578918, 44.9480295181, 44.9480724335}}, + {"grxrc9by8g4", []float64{88.5605496168, 88.5605509579, -23.4877046943, -23.4877033532}}, + {"6z0", []float64{-5.625, -4.21875, -56.25, -54.84375}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"6npe6", []float64{-10.6787109375, -10.634765625, -79.365234375, -79.3212890625}}, + {"04wzmgz23", []float64{-74.6424436569, -74.6424007416, -170.245127678, -170.245084763}}, + {"dmfzk9", []float64{33.6236572266, 33.6291503906, -74.6850585938, -74.6740722656}}, + {"eeu", []float64{21.09375, 22.5, -16.875, -15.46875}}, + {"84bd9k3t", []float64{15.9324073792, 15.9325790405, -179.239883423, -179.2395401}}, + {"7q6ywwxq2kc", []float64{-8.664367944, -8.6643666029, -29.5871995389, -29.5871981978}}, + {"bve6jyuwk", []float64{76.327214241, 76.3272571564, -141.420650482, -141.420607567}}, + {"h558", []float64{-73.125, -72.94921875, 4.921875, 5.2734375}}, + {"sk4f3d2h1vq", []float64{22.9085822403, 22.9085835814, 15.1831886172, 15.1831899583}}, + {"tffnvn", []float64{16.6882324219, 16.6937255859, 81.7822265625, 81.7932128906}}, + {"eppvmm886m", []float64{40.3281337023, 40.3281390667, -33.8700664043, -33.8700556755}}, + {"d2w", []float64{2.8125, 4.21875, -70.3125, -68.90625}}, + {"sd3", []float64{12.65625, 14.0625, 23.90625, 25.3125}}, + {"q73kk", []float64{-25.9716796875, -25.927734375, 103.18359375, 103.227539062}}, + {"wtz5yx", []float64{33.0413818359, 33.046875, 122.629394531, 122.640380859}}, + {"xw8z", []float64{37.79296875, 37.96875, 158.5546875, 158.90625}}, + {"dyhuchkt7qr", []float64{34.6092416346, 34.6092429757, -49.5200385153, -49.5200371742}}, + {"pyb1es47f", []float64{-51.7449617386, -51.7449188232, 168.906984329, 168.907027245}}, + {"v6jcgfj1nus", []float64{56.5687993169, 56.568800658, 64.5078939199, 64.507895261}}, + {"xxe", []float64{42.1875, 43.59375, 161.71875, 163.125}}, + {"7xdvcext7rq", []float64{-1.78159162402, -1.78159028292, -18.5564473271, -18.556445986}}, + {"1f35b7w", []float64{-76.6653442383, -76.6639709473, -99.8245239258, -99.8231506348}}, + {"he675b8pjek", []float64{-71.187440604, -71.1874392629, 25.8290988207, 25.8291001618}}, + {"pzj3", []float64{-50.44921875, -50.2734375, 176.1328125, 176.484375}}, + {"1s1m8j10zwq3", []float64{-66.5055748634, -66.5055746958, -110.740483962, -110.740483627}}, + {"sk4xsg4ufxm", []float64{23.8356931508, 23.8356944919, 14.9782557786, 14.9782571197}}, + {"r3m2ker83", []float64{-37.906908989, -37.9068660736, 153.840909004, 153.84095192}}, + {"p7bj", []float64{-68.02734375, -67.8515625, 146.25, 146.6015625}}, + {"4wws597u", []float64{-52.7268218994, -52.726650238, -58.2004165649, -58.2000732422}}, + {"u9rk", []float64{52.734375, 52.91015625, 32.6953125, 33.046875}}, + {"2mv856bp9uj4", []float64{-12.6398345456, -12.6398343779, -160.872720927, -160.872720592}}, + {"57th4543xnbs", []float64{-69.5926011354, -69.5926009677, -26.6274683923, -26.627468057}}, + {"8pct", []float64{44.47265625, 44.6484375, -177.890625, -177.5390625}}, + {"me0skjqbk8", []float64{-27.3490476608, -27.3490422964, 68.3883690834, 68.3883798122}}, + {"30s74", []float64{-41.66015625, -41.6162109375, -128.935546875, -128.891601562}}, + {"9r0h9fedn", []float64{40.1800918579, 40.1801347733, -123.668031693, -123.667988777}}, + {"9v6gvte7r", []float64{30.2211999893, 30.2212429047, -97.136349678, -97.1363067627}}, + {"j5d", []float64{-70.3125, -68.90625, 47.8125, 49.21875}}, + {"hf8ge", []float64{-75.322265625, -75.2783203125, 34.9365234375, 34.98046875}}, + {"hf2hmz", []float64{-76.5582275391, -76.552734375, 34.0026855469, 34.013671875}}, + {"5wc405", []float64{-51.6632080078, -51.6577148438, -21.09375, -21.0827636719}}, + {"x2dk49y7p8", []float64{3.52575302124, 3.52575838566, 149.532830715, 149.532841444}}, + {"350jjfux7dbk", []float64{-27.2297275811, -27.2297274135, -134.740984105, -134.740983769}}, + {"04wd", []float64{-75.5859375, -75.41015625, -170.859375, -170.5078125}}, + {"1zxmhstk", []float64{-46.9081878662, -46.9080162048, -90.8497238159, -90.8493804932}}, + {"b2sj26ddce3", []float64{48.7495739758, 48.7495753169, -163.11051473, -163.110513389}}, + {"vznd8mz363", []float64{84.8462587595, 84.8462641239, 87.9116642475, 87.9116749763}}, + {"f", []float64{45.0, 90.0, -90.0, -45.0}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"kw", []float64{-11.25, -5.625, 22.5, 33.75}}, + {"1s0nq579", []float64{-66.3833427429, -66.3831710815, -112.231521606, -112.231178284}}, + {"mzky2scd", []float64{-3.09368133545, -3.09350967407, 85.4537200928, 85.4540634155}}, + {"kctpzwx2rxg", []float64{-35.1644052565, -35.1644039154, 41.121122092, 41.1211234331}}, + {"19", []float64{-84.375, -78.75, -112.5, -101.25}}, + {"pmg4wc8pn", []float64{-57.2073554993, -57.2073125839, 150.765638351, 150.765681267}}, + {"sxcn0yd0jck", []float64{44.6841497719, 44.684151113, 23.9422076941, 23.9422090352}}, + {"000dsj4g", []float64{-89.5325660706, -89.5323944092, -179.1173172, -179.116973877}}, + {"pgv0", []float64{-68.90625, -68.73046875, 175.78125, 176.1328125}}, + {"dhw0935d8", []float64{25.4063129425, 25.4063558578, -81.5027618408, -81.5027189255}}, + {"4gvz08kbk9", []float64{-67.6743596792, -67.6743543148, -48.1353735924, -48.1353628635}}, + {"djz4g1m", []float64{32.8340148926, 32.8353881836, -80.0175476074, -80.0161743164}}, + {"x4yhn1h5m1z", []float64{16.1779354513, 16.1779367924, 143.706889004, 143.706890345}}, + {"9t", []float64{28.125, 33.75, -112.5, -101.25}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"4fgytpvp2v", []float64{-73.3448284864, -73.344823122, -50.7499372959, -50.7499265671}}, + {"jggxx", []float64{-67.587890625, -67.5439453125, 83.9794921875, 84.0234375}}, + {"mye8t1", []float64{-8.34411621094, -8.33862304688, 83.8916015625, 83.9025878906}}, + {"bssun7dstj", []float64{71.0356503725, 71.0356557369, -150.542006493, -150.541995764}}, + {"ehhv", []float64{23.37890625, 23.5546875, -38.3203125, -37.96875}}, + {"484tc", []float64{-88.9892578125, -88.9453125, -63.9404296875, -63.896484375}}, + {"d4tjtv", []float64{15.0567626953, 15.0622558594, -82.7160644531, -82.705078125}}, + {"2z", []float64{-5.625, 0.0, -146.25, -135.0}}, + {"t5ey32", []float64{20.7861328125, 20.7916259766, 50.3283691406, 50.3393554688}}, + {"um", []float64{73.125, 78.75, 11.25, 22.5}}, + {"pe", []float64{-73.125, -67.5, 157.5, 168.75}}, + {"2x05zw6z", []float64{-4.93028640747, -4.93011474609, -157.166633606, -157.166290283}}, + {"67", []float64{-28.125, -22.5, -78.75, -67.5}}, + {"dxfr3yndexbg", []float64{44.9015942775, 44.9015944451, -64.249955602, -64.2499552667}}, + {"y87", []float64{46.40625, 47.8125, 116.71875, 118.125}}, + {"2h29w", []float64{-20.830078125, -20.7861328125, -179.033203125, -178.989257812}}, + {"cv", []float64{73.125, 78.75, -101.25, -90.0}}, + {"jcx2m1mjy9e", []float64{-81.5106931329, -81.5106917918, 89.1721884906, 89.1721898317}}, + {"ryj9w", []float64{-10.986328125, -10.9423828125, 176.748046875, 176.791992188}}, + {"g5sftxrhf40", []float64{65.1676046848, 65.1676060259, -38.0689144135, -38.0689130723}}, + {"nhs95z98z", []float64{-64.4703912735, -64.4703483582, 96.4952802658, 96.4953231812}}, + {"s7n00se8u3n", []float64{16.8998533487, 16.8998546898, 19.7144696116, 19.7144709527}}, + {"4310r6m", []float64{-84.3186950684, -84.3173217773, -77.0182800293, -77.0169067383}}, + {"7kkmp", []float64{-20.21484375, -20.1708984375, -27.4658203125, -27.421875}}, + {"3dvnpf13665", []float64{-28.4653508663, -28.4653495252, -105.126356632, -105.12635529}}, + {"1cv2hegqd1s7", []float64{-80.1345262863, -80.1345261186, -93.6648788676, -93.6648785323}}, + {"uy0yttxwk65", []float64{79.9238741398, 79.9238754809, 35.0568728149, 35.056874156}}, + {"166cs2etxffy", []float64{-77.0763716474, -77.0763714798, -119.690902121, -119.690901786}}, + {"bm7n7", []float64{75.6298828125, 75.673828125, -164.399414062, -164.35546875}}, + {"5r7q", []float64{-48.1640625, -47.98828125, -29.1796875, -28.828125}}, + {"ymjqjv6", []float64{74.2085266113, 74.2098999023, 108.888244629, 108.88961792}}, + {"jx95kpvmv9", []float64{-47.1976464987, -47.1976411343, 69.0894770622, 69.0894877911}}, + {"f1m4m9", []float64{52.4322509766, 52.4377441406, -82.7270507812, -82.7160644531}}, + {"0cdr1wfep", []float64{-80.2944374084, -80.2943944931, -143.016285896, -143.016242981}}, + {"fe4q2v7", []float64{63.0024719238, 63.0038452148, -64.2988586426, -64.2974853516}}, + {"9pxfyb9p", []float64{42.6748466492, 42.6750183105, -123.80355835, -123.803215027}}, + {"8w088r", []float64{33.8763427734, 33.8818359375, -156.785888672, -156.774902344}}, + {"u569fj", []float64{63.6163330078, 63.6218261719, 3.603515625, 3.61450195312}}, + {"smvm0syj0kst", []float64{33.2496320643, 33.2496322319, 18.6630416662, 18.6630420014}}, + {"vx", []float64{84.375, 90.0, 67.5, 78.75}}, + {"zj", []float64{73.125, 78.75, 135.0, 146.25}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"69n85w0", []float64{-39.3420410156, -39.3406677246, -58.2055664062, -58.2041931152}}, + {"ywmj", []float64{81.03515625, 81.2109375, 119.53125, 119.8828125}}, + {"2717c52t", []float64{-27.4471092224, -27.446937561, -166.947555542, -166.947212219}}, + {"t1h", []float64{5.625, 7.03125, 50.625, 52.03125}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"5xphd2", []float64{-49.833984375, -49.8284912109, -12.5573730469, -12.5463867188}}, + {"xy8", []float64{36.5625, 37.96875, 168.75, 170.15625}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"3pet007v7qb", []float64{-1.93128302693, -1.93128168583, -130.072835684, -130.072834343}}, + {"dyuz", []float64{39.19921875, 39.375, -49.5703125, -49.21875}}, + {"6r", []float64{-5.625, 0.0, -78.75, -67.5}}, + {"y8v06s5", []float64{49.2846679688, 49.2860412598, 119.645233154, 119.646606445}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"mfuy2m", []float64{-28.4051513672, -28.3996582031, 85.4406738281, 85.4516601562}}, + {"dnp2u", []float64{33.8818359375, 33.92578125, -79.62890625, -79.5849609375}}, + {"g4u3ehx6", []float64{60.757484436, 60.7576560974, -38.8816452026, -38.8813018799}}, + {"q14h9tg88tx", []float64{-38.5522833467, -38.5522820055, 92.8832553327, 92.8832566738}}, + {"d4m6rn", []float64{13.0847167969, 13.0902099609, -82.3095703125, -82.2985839844}}, + {"64", []float64{-33.75, -28.125, -90.0, -78.75}}, + {"y2nzb", []float64{46.3623046875, 46.40625, 110.7421875, 110.786132812}}, + {"yhs5m8ytcgxb", []float64{70.8889147639, 70.8889149316, 95.8757111058, 95.875711441}}, + {"ytp9j8f0dv8", []float64{73.305016458, 73.3050177991, 123.291438818, 123.291440159}}, + {"pxcpm2h", []float64{-45.1318359375, -45.1304626465, 159.142456055, 159.143829346}}, + {"5zyf9", []float64{-45.966796875, -45.9228515625, -1.7138671875, -1.669921875}}, + {"wmz7v2", []float64{33.0029296875, 33.0084228516, 111.676025391, 111.687011719}}, + {"3fb7hkc8wus", []float64{-28.9777037501, -28.977702409, -100.709314942, -100.709313601}}, + {"ssstmsrv", []float64{26.2595558167, 26.259727478, 29.0804672241, 29.0808105469}}, + {"21", []float64{-39.375, -33.75, -180.0, -168.75}}, + {"w0du7r8257", []float64{3.60078513622, 3.60079050064, 94.0104925632, 94.0105032921}}, + {"fhx", []float64{70.3125, 71.71875, -80.15625, -78.75}}, + {"1xd", []float64{-47.8125, -46.40625, -109.6875, -108.28125}}, + {"s040p9v3shb2", []float64{0.00989601016045, 0.00989617779851, 3.14947161824, 3.14947195351}}, + {"gq", []float64{78.75, 84.375, -33.75, -22.5}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"17b4wsgdq0q", []float64{-68.4403167665, -68.4403154254, -123.459283412, -123.45928207}}, + {"x3qs", []float64{7.734375, 7.91015625, 155.390625, 155.7421875}}, + {"rc", []float64{-39.375, -33.75, 168.75, 180.0}}, + {"sfxjv06b9", []float64{15.0747013092, 15.0747442245, 43.8172960281, 43.8173389435}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"ngveyg", []float64{-68.2305908203, -68.2250976562, 131.781005859, 131.791992188}}, + {"pmxd5bd39qp", []float64{-58.7079012394, -58.7078998983, 156.964822859, 156.9648242}}, + {"xhw6", []float64{25.6640625, 25.83984375, 143.7890625, 144.140625}}, + {"bc6vx6", []float64{53.0090332031, 53.0145263672, -142.064208984, -142.053222656}}, + {"uuxy5sbu", []float64{71.3939666748, 71.3941383362, 44.803276062, 44.8036193848}}, + {"yu", []float64{67.5, 73.125, 123.75, 135.0}}, + {"610pf9c3ebh", []float64{-38.0028247833, -38.0028234422, -89.888253808, -89.8882524669}}, + {"9d", []float64{11.25, 16.875, -112.5, -101.25}}, + {"1s0tk4yp", []float64{-66.5608406067, -66.5606689453, -111.612854004, -111.612510681}}, + {"ss7yjqhs5su", []float64{24.9946086109, 24.994609952, 28.0104857683, 28.0104871094}}, + {"krqww1d0kp", []float64{-3.06785166264, -3.06784629822, 20.6572151184, 20.6572258472}}, + {"4yzg18c07qnm", []float64{-51.4997104369, -51.4997102693, -45.2841233835, -45.2841230482}}, + {"5wf241", []float64{-52.0257568359, -52.0202636719, -19.248046875, -19.2370605469}}, + {"gxrh60ce", []float64{86.5329551697, 86.5331268311, -12.5662994385, -12.5659561157}}, + {"5rzrgr9qf", []float64{-45.0015878677, -45.0015449524, -23.4100627899, -23.4100198746}}, + {"u3", []float64{50.625, 56.25, 11.25, 22.5}}, + {"8tm4uz7c", []float64{30.0546455383, 30.0548171997, -150.254859924, -150.254516602}}, + {"wz5f3", []float64{39.7705078125, 39.814453125, 129.067382812, 129.111328125}}, + {"ckz", []float64{71.71875, 73.125, -113.90625, -112.5}}, + {"h4s31", []float64{-75.76171875, -75.7177734375, 6.0205078125, 6.064453125}}, + {"hguje", []float64{-67.939453125, -67.8955078125, 39.5068359375, 39.55078125}}, + {"4rnfer", []float64{-50.1470947266, -50.1416015625, -69.1149902344, -69.1040039062}}, + {"gj", []float64{73.125, 78.75, -45.0, -33.75}}, + {"04", []float64{-78.75, -73.125, -180.0, -168.75}}, + {"kvj", []float64{-16.875, -15.46875, 40.78125, 42.1875}}, + {"7c3p", []float64{-36.73828125, -36.5625, -9.84375, -9.4921875}}, + {"rdw55", []float64{-30.41015625, -30.3662109375, 166.069335938, 166.11328125}}, + {"7spe18wk094b", []float64{-21.969217658, -21.9692174904, -11.8785988167, -11.8785984814}}, + {"uxumm", []float64{89.5166015625, 89.560546875, 28.6962890625, 28.740234375}}, + {"1n0sh0", []float64{-55.546875, -55.5413818359, -134.12109375, -134.110107422}}, + {"cphmy", []float64{85.3857421875, 85.4296875, -128.759765625, -128.715820312}}, + {"sd", []float64{11.25, 16.875, 22.5, 33.75}}, + {"h6jbb2gxyd0", []float64{-78.6127030849, -78.6127017438, 19.3520092964, 19.3520106375}}, + {"x3", []float64{5.625, 11.25, 146.25, 157.5}}, + {"yv289c0nbs", []float64{74.625813961, 74.6258193254, 124.530050755, 124.530061483}}, + {"g1fxbp5", []float64{56.2445068359, 56.245880127, -41.480255127, -41.4788818359}}, + {"bqdh", []float64{82.265625, 82.44140625, -165.9375, -165.5859375}}, + {"w558neznn3", []float64{16.8966346979, 16.8966400623, 95.2174007893, 95.2174115181}}, + {"up", []float64{84.375, 90.0, 0.0, 11.25}}, + {"pmy73pm2r", []float64{-57.0450925827, -57.0450496674, 155.090517998, 155.090560913}}, + {"jzerkufsjnh", []float64{-46.5112745762, -46.5112732351, 83.5327059031, 83.5327072442}}, + {"8eks", []float64{18.984375, 19.16015625, -151.171875, -150.8203125}}, + {"3rryyvfxc", []float64{-2.99931049347, -2.99926757812, -112.551455498, -112.551412582}}, + {"vn0cz", []float64{79.0576171875, 79.1015625, 46.3623046875, 46.40625}}, + {"skb5cnez", []float64{27.4148368835, 27.4150085449, 11.2990951538, 11.2994384766}}, + {"3p5cu0yju", []float64{-5.31227588654, -5.31223297119, -129.542369843, -129.542326927}}, + {"8yrwss1y2s", []float64{36.3218951225, 36.3219004869, -135.502946377, -135.502935648}}, + {"7x9kys4ydp", []float64{-1.95441305637, -1.95440769196, -20.4526805878, -20.4526698589}}, + {"u7gscks3", []float64{66.9536018372, 66.9537734985, 16.2326431274, 16.2329864502}}, + {"30c8pz7", []float64{-40.7414245605, -40.7400512695, -132.545928955, -132.544555664}}, + {"umertwj6wxuc", []float64{77.2892892547, 77.2892894223, 16.0695068166, 16.0695071518}}, + {"n6", []float64{-78.75, -73.125, 101.25, 112.5}}, + {"br8dqrhg058f", []float64{87.6219940558, 87.6219942234, -167.765692659, -167.765692323}}, + {"tp", []float64{39.375, 45.0, 45.0, 56.25}}, + {"33uy", []float64{-34.1015625, -33.92578125, -117.0703125, -116.71875}}, + {"u3ps1jnxg", []float64{51.356921196, 51.3569641113, 21.8498754501, 21.8499183655}}, + {"nqwmqymbyz", []float64{-52.4801498652, -52.4801445007, 110.343879461, 110.34389019}}, + {"wfugcgd", []float64{16.1471557617, 16.1485290527, 130.509338379, 130.51071167}}, + {"vp7g2eg", []float64{86.3731384277, 86.3745117188, 50.2995300293, 50.3009033203}}, + {"zz", []float64{84.375, 90.0, 168.75, 180.0}}, + {"859t0xmm6gwk", []float64{20.6071523577, 20.6071525253, -177.861316167, -177.861315832}}, + {"bvjnf49wp1hu", []float64{74.3262923509, 74.3262925185, -139.128492661, -139.128492326}}, + {"08ybs", []float64{-85.693359375, -85.6494140625, -147.83203125, -147.788085938}}, + {"dr908p7", []float64{42.3152160645, 42.3165893555, -77.339630127, -77.3382568359}}, + {"gufpbsu", []float64{73.1071472168, 73.1085205078, -8.41003417969, -8.40866088867}}, + {"623c388eg2t", []float64{-43.3706304431, -43.370629102, -76.2223117054, -76.2223103642}}, + {"8rc", []float64{43.59375, 45.0, -167.34375, -165.9375}}, + {"h4", []float64{-78.75, -73.125, 0.0, 11.25}}, + {"x84", []float64{0.0, 1.40625, 160.3125, 161.71875}}, + {"bbxgcx6nyzk", []float64{48.5127027333, 48.5127040744, -135.282602906, -135.282601565}}, + {"tqg06239nrht", []float64{38.014278654, 38.0142788216, 60.5699611455, 60.5699614808}}, + {"05fd", []float64{-68.5546875, -68.37890625, -176.484375, -176.1328125}}, + {"wuc", []float64{26.71875, 28.125, 125.15625, 126.5625}}, + {"m3hh", []float64{-38.671875, -38.49609375, 61.875, 62.2265625}}, + {"m2w4ru", []float64{-41.7700195312, -41.7645263672, 65.0280761719, 65.0390625}}, + {"4cz3k", []float64{-79.9365234375, -79.892578125, -45.87890625, -45.8349609375}}, + {"jqefz9v", []float64{-52.9444885254, -52.9431152344, 61.8598937988, 61.8612670898}}, + {"qqcnf60wjf", []float64{-5.83269953728, -5.83269417286, 102.756060362, 102.756071091}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"n8qen549x9vr", []float64{-88.0496587045, -88.0496585369, 121.908059008, 121.908059344}}, + {"g01nwm2wyj", []float64{46.1726027727, 46.1726081371, -43.3181476593, -43.3181369305}}, + {"6xc", []float64{-1.40625, 0.0, -66.09375, -64.6875}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"nmjjnhz67", []float64{-60.9696149826, -60.9695720673, 108.555006981, 108.555049896}}, + {"2gzwv1zggqg", []float64{-22.7094335854, -22.7094322443, -135.472611934, -135.472610593}}, + {"dt", []float64{28.125, 33.75, -67.5, -56.25}}, + {"v33ysf2y0s", []float64{53.1872391701, 53.1872445345, 58.9207291603, 58.9207398891}}, + {"fwy6tccjp3", []float64{83.4186798334, 83.4186851978, -58.4565675259, -58.456556797}}, + {"qug6fjx4ue8k", []float64{-17.7671476454, -17.7671474777, 128.418009616, 128.418009952}}, + {"8y59duq21", []float64{34.0370178223, 34.0370607376, -141.198649406, -141.198606491}}, + {"pxgfrpkhdnk", []float64{-45.9701107442, -45.9701094031, 163.086639047, 163.086640388}}, + {"91b", []float64{9.84375, 11.25, -135.0, -133.59375}}, + {"v83mnvugmh", []float64{47.3173213005, 47.3173266649, 69.5611810684, 69.5611917973}}, + {"k76cem9", []float64{-26.4248657227, -26.4234924316, 15.2613830566, 15.2627563477}}, + {"ydjq0mk", []float64{57.3335266113, 57.3348999023, 119.899291992, 119.900665283}}, + {"cdsuf", []float64{59.8974609375, 59.94140625, -105.732421875, -105.688476562}}, + {"tuyz3", []float64{27.9931640625, 28.037109375, 88.2861328125, 88.330078125}}, + {"r7r2skwfj00", []float64{-26.605796814, -26.6057954729, 156.641564369, 156.64156571}}, + {"8hx507p9f5x", []float64{25.8566424251, 25.8566437662, -170.134868771, -170.13486743}}, + {"cc", []float64{50.625, 56.25, -101.25, -90.0}}, + {"pkuuvxz1z3", []float64{-62.4034112692, -62.4034059048, 153.181310892, 153.181321621}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"7vvz9gbf", []float64{-11.316947937, -11.3167762756, -3.08612823486, -3.08578491211}}, + {"54r", []float64{-77.34375, -75.9375, -35.15625, -33.75}}, + {"5r0mm0pwj1j", []float64{-49.7011131048, -49.7011117637, -33.1681899726, -33.1681886315}}, + {"mx", []float64{-5.625, 0.0, 67.5, 78.75}}, + {"rm39jwc", []float64{-15.2558898926, -15.2545166016, 148.60244751, 148.603820801}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"4gqrkyvqc", []float64{-70.4060983658, -70.4060554504, -47.2449445724, -47.2449016571}}, + {"9s36btfr9", []float64{24.4225215912, 24.4225645065, -110.717082024, -110.717039108}}, + {"dxt5", []float64{42.71484375, 42.890625, -60.46875, -60.1171875}}, + {"21x8tven9", []float64{-36.4432811737, -36.4432382584, -169.196276665, -169.196233749}}, + {"7hh", []float64{-22.5, -21.09375, -39.375, -37.96875}}, + {"v7nm8q7s", []float64{62.8768157959, 62.8769874573, 65.0548553467, 65.0551986694}}, + {"e9qxh", []float64{8.26171875, 8.3056640625, -13.18359375, -13.1396484375}}, + {"273", []float64{-26.71875, -25.3125, -167.34375, -165.9375}}, + {"p99qpkcvb", []float64{-80.4807329178, -80.4806900024, 159.578819275, 159.57886219}}, + {"q2062f9csybw", []float64{-44.5904645696, -44.590464402, 101.637129262, 101.637129597}}, + {"btsq32", []float64{77.0361328125, 77.0416259766, -151.468505859, -151.457519531}}, + {"1cj5", []float64{-83.84765625, -83.671875, -94.21875, -93.8671875}}, + {"j71pemzwqxt9", []float64{-71.7739416473, -71.7739414796, 57.8096582741, 57.8096586093}}, + {"qnqcbndu0nf", []float64{-9.49970439076, -9.49970304966, 99.4959667325, 99.4959680736}}, + {"2v7cm6junqb", []float64{-15.237314254, -15.2373129129, -140.737684965, -140.737683624}}, + {"e7", []float64{16.875, 22.5, -33.75, -22.5}}, + {"s91p45", []float64{6.87194824219, 6.87744140625, 23.994140625, 24.0051269531}}, + {"xwk1de36", []float64{35.438117981, 35.4382896423, 163.236579895, 163.236923218}}, + {"gf2", []float64{57.65625, 59.0625, -11.25, -9.84375}}, + {"d1m", []float64{7.03125, 8.4375, -82.96875, -81.5625}}, + {"0b", []float64{-90.0, -84.375, -146.25, -135.0}}, + {"rw4", []float64{-11.25, -9.84375, 160.3125, 161.71875}}, + {"74pk9xsb", []float64{-32.9177856445, -32.9176139832, -34.7322463989, -34.7319030762}}, + {"4ghe0cn8s", []float64{-72.5920772552, -72.5920343399, -49.8798179626, -49.8797750473}}, + {"tw4", []float64{33.75, 35.15625, 70.3125, 71.71875}}, + {"gevx3", []float64{67.3681640625, 67.412109375, -14.7216796875, -14.677734375}}, + {"kw8w6yqc1ks", []float64{-7.30433911085, -7.30433776975, 23.3333033323, 23.3333046734}}, + {"vbdjwzpg7", []float64{48.8183069229, 48.8183498383, 81.8699026108, 81.8699455261}}, + {"tp", []float64{39.375, 45.0, 45.0, 56.25}}, + {"751q", []float64{-27.0703125, -26.89453125, -43.2421875, -42.890625}}, + {"yeurzu", []float64{67.4780273438, 67.4835205078, 118.817138672, 118.828125}}, + {"svx", []float64{30.9375, 32.34375, 43.59375, 45.0}}, + {"4yrg1w", []float64{-54.2834472656, -54.2779541016, -45.2856445312, -45.2746582031}}, + {"h6xmxenmzkc", []float64{-74.9532110989, -74.9532097578, 21.7837978899, 21.7837992311}}, + {"j5uzmqughwj1", []float64{-67.5942097418, -67.5942095742, 51.9171233475, 51.9171236828}}, + {"26trngxu", []float64{-29.6871185303, -29.6869468689, -161.059913635, -161.059570312}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"h58jxs7g", []float64{-69.3218421936, -69.3216705322, 0.334739685059, 0.335083007812}}, + {"tg2kgbk3", []float64{19.1177558899, 19.1179275513, 79.2721939087, 79.2725372314}}, + {"x34hfu", []float64{6.48193359375, 6.48742675781, 149.183349609, 149.194335938}}, + {"774j", []float64{-27.24609375, -27.0703125, -30.9375, -30.5859375}}, + {"yf", []float64{56.25, 61.875, 123.75, 135.0}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"e273k4gje9s", []float64{1.64203494787, 1.64203628898, -28.9996308088, -28.9996294677}}, + {"e", []float64{0.0, 45.0, -45.0, 0.0}}, + {"hx", []float64{-50.625, -45.0, 22.5, 33.75}}, + {"wz", []float64{39.375, 45.0, 123.75, 135.0}}, + {"w7p94b", []float64{17.05078125, 17.0562744141, 111.917724609, 111.928710938}}, + {"69sgzyb46pbs", []float64{-35.8658129722, -35.8658128045, -60.4796498269, -60.4796494916}}, + {"7f7m8k2u5", []float64{-31.3529205322, -31.3528776169, -6.66754245758, -6.66749954224}}, + {"gq4n7m", []float64{79.8760986328, 79.8815917969, -30.7946777344, -30.7836914062}}, + {"c1srg1pz8kt", []float64{54.8066094518, 54.8066107929, -128.880941123, -128.880939782}}, + {"b2h1dy", []float64{45.2966308594, 45.3021240234, -163.004150391, -162.993164062}}, + {"7170zy", []float64{-37.8039550781, -37.7984619141, -40.4406738281, -40.4296875}}, + {"p09", []float64{-87.1875, -85.78125, 136.40625, 137.8125}}, + {"sw970ux6", []float64{37.114906311, 37.1150779724, 24.3007278442, 24.301071167}}, + {"rc8hsdptqn", []float64{-35.7595646381, -35.7595592737, 168.958311081, 168.95832181}}, + {"qp3jujqc1", []float64{-3.17899703979, -3.17895412445, 91.5913438797, 91.591386795}}, + {"dn7n9krj2tgg", []float64{36.3231066428, 36.3231068105, -85.7166788355, -85.7166785002}}, + {"23e7vwt", []float64{-35.8676147461, -35.8662414551, -163.931121826, -163.929748535}}, + {"um", []float64{73.125, 78.75, 11.25, 22.5}}, + {"p8ws1", []float64{-86.484375, -86.4404296875, 166.684570312, 166.728515625}}, + {"vqt63tm6xx", []float64{81.9873136282, 81.9873189926, 63.7062621117, 63.7062728405}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"bm20d", []float64{74.619140625, 74.6630859375, -168.662109375, -168.618164062}}, + {"p3t4k9c0", []float64{-81.1573791504, -81.157207489, 153.480377197, 153.48072052}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"8rsnr3k0", []float64{43.2929992676, 43.293170929, -162.80090332, -162.800559998}}, + {"b739vykqw", []float64{63.6243152618, 63.6243581772, -166.381845474, -166.381802559}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"13zrrq", []float64{-78.8488769531, -78.8433837891, -113.236083984, -113.225097656}}, + {"6yk9jt", []float64{-9.64050292969, -9.63500976562, -49.6801757812, -49.6691894531}}, + {"zmn0", []float64{73.125, 73.30078125, 154.6875, 155.0390625}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"ps151cxkk8", []float64{-66.9636869431, -66.9636815786, 158.993303776, 158.993314505}}, + {"tn", []float64{33.75, 39.375, 45.0, 56.25}}, + {"u7", []float64{61.875, 67.5, 11.25, 22.5}}, + {"55yuz", []float64{-68.0712890625, -68.02734375, -35.2001953125, -35.15625}}, + {"x0p3grv8k", []float64{0.350232124329, 0.350275039673, 145.345859528, 145.345902443}}, + {"y5c6gjhshg", []float64{66.6053169966, 66.605322361, 91.896032095, 91.8960428238}}, + {"6fd2j", []float64{-30.9375, -30.8935546875, -52.8662109375, -52.822265625}}, + {"gegbfkt19cj6", []float64{66.2505683675, 66.2505685352, -17.1207369491, -17.1207366139}}, + {"k7", []float64{-28.125, -22.5, 11.25, 22.5}}, + {"y1ydbr", []float64{55.3656005859, 55.37109375, 99.1516113281, 99.1625976562}}, + {"byqsd", []float64{80.947265625, 80.9912109375, -137.021484375, -136.977539062}}, + {"mfcbxhf", []float64{-29.4172668457, -29.4158935547, 81.5213012695, 81.5226745605}}, + {"yxwd5901", []float64{87.5447273254, 87.5448989868, 121.794433594, 121.794776917}}, + {"24k7s7y0gqgj", []float64{-31.7077504657, -31.7077502981, -173.828286678, -173.828286342}}, + {"9031fbu", []float64{1.71798706055, 1.71936035156, -133.467407227, -133.466033936}}, + {"xqusuduvmc", []float64{38.8197237253, 38.8197290897, 152.782648802, 152.782659531}}, + {"5bxw", []float64{-86.1328125, -85.95703125, -0.703125, -0.3515625}}, + {"xw593h52f", []float64{33.9918279648, 33.9918708801, 162.470369339, 162.470412254}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"9xxm2s", []float64{43.1323242188, 43.1378173828, -102.282714844, -102.271728516}}, + {"byf7t", []float64{83.583984375, 83.6279296875, -142.866210938, -142.822265625}}, + {"v6", []float64{56.25, 61.875, 56.25, 67.5}}, + {"yh", []float64{67.5, 73.125, 90.0, 101.25}}, + {"d6k43xp", []float64{13.0902099609, 13.091583252, -73.0494689941, -73.0480957031}}, + {"k4m5h", []float64{-31.81640625, -31.7724609375, 7.20703125, 7.2509765625}}, + {"r1t7", []float64{-36.03515625, -35.859375, 142.3828125, 142.734375}}, + {"cs4kvrjdkm7", []float64{68.3738274872, 68.3738288283, -109.097485095, -109.097483754}}, + {"unu", []float64{82.96875, 84.375, 5.625, 7.03125}}, + {"59xp", []float64{-80.33203125, -80.15625, -12.65625, -12.3046875}}, + {"542p4hbm", []float64{-76.0863304138, -76.0861587524, -44.9117660522, -44.9114227295}}, + {"5j", []float64{-61.875, -56.25, -45.0, -33.75}}, + {"v3", []float64{50.625, 56.25, 56.25, 67.5}}, + {"mstr13c0", []float64{-18.4474182129, -18.4472465515, 74.9391174316, 74.9394607544}}, + {"wcvtdg61swv8", []float64{10.8286933601, 10.8286935277, 131.608171687, 131.608172022}}, + {"3cm40m0w91z3", []float64{-37.5885963254, -37.5885961577, -94.207024388, -94.2070240527}}, + {"m9fup0", []float64{-34.453125, -34.4476318359, 71.6748046875, 71.6857910156}}, + {"jx", []float64{-50.625, -45.0, 67.5, 78.75}}, + {"7myut8cg", []float64{-11.8605995178, -11.8604278564, -24.013710022, -24.0133666992}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"5dgu11uuf12g", []float64{-73.8176893629, -73.8176891953, -17.1760072187, -17.1760068834}}, + {"qp1ens1zqg", []float64{-5.07442295551, -5.07441759109, 92.3977124691, 92.3977231979}}, + {"vusxu8ewwbrz", []float64{71.6786695831, 71.6786697507, 85.2809854969, 85.2809858322}}, + {"8qjtgd1q", []float64{34.7727584839, 34.7729301453, -160.860099792, -160.85975647}}, + {"60dppvw", []float64{-40.9268188477, -40.9254455566, -86.838684082, -86.837310791}}, + {"tygxz83s2", []float64{39.3331575394, 39.3332004547, 84.0035247803, 84.0035676956}}, + {"e0qwrc", []float64{2.51037597656, 2.51586914062, -35.5187988281, -35.5078125}}, + {"5wyh2qbz", []float64{-51.2458992004, -51.2457275391, -14.0504837036, -14.0501403809}}, + {"0zqs", []float64{-48.515625, -48.33984375, -137.109375, -136.7578125}}, + {"n5ss", []float64{-69.609375, -69.43359375, 96.328125, 96.6796875}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"v9q84x9n8ktx", []float64{52.0735898428, 52.0735900104, 76.7518796772, 76.7518800125}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"2eh8wf4ktz", []float64{-28.0253130198, -28.0253076553, -150.871907473, -150.871896744}}, + {"cr96dg281x", []float64{87.6448434591, 87.6448488235, -121.870586872, -121.870576143}}, + {"56u", []float64{-74.53125, -73.125, -28.125, -26.71875}}, + {"628vt", []float64{-41.220703125, -41.1767578125, -77.4755859375, -77.431640625}}, + {"0heb", []float64{-64.6875, -64.51171875, -174.7265625, -174.375}}, + {"8q5skg4mk", []float64{34.5144510269, 34.5144939423, -163.616123199, -163.616080284}}, + {"7x0pw97495hw", []float64{-4.2993279174, -4.29932774976, -22.2101866454, -22.2101863101}}, + {"1j4vjyz1dqx", []float64{-60.9587225318, -60.9587211907, -130.870407969, -130.870406628}}, + {"u4q1szh", []float64{57.9583740234, 57.9597473145, 8.65173339844, 8.65310668945}}, + {"3v3dbm2uv2", []float64{-14.9556970596, -14.9556916952, -99.1283833981, -99.1283726692}}, + {"te3jtfwjnq", []float64{19.2626702785, 19.262675643, 69.1674435139, 69.1674542427}}, + {"sgvgx3ey6qe", []float64{21.7183318734, 21.7183332145, 42.1597914398, 42.1597927809}}, + {"2sw", []float64{-19.6875, -18.28125, -149.0625, -147.65625}}, + {"wzy2pqu8e", []float64{43.6309146881, 43.6309576035, 132.863974571, 132.864017487}}, + {"dk2849", []float64{23.9117431641, 23.9172363281, -77.9370117188, -77.9260253906}}, + {"0d65", []float64{-76.81640625, -76.640625, -154.6875, -154.3359375}}, + {"84", []float64{11.25, 16.875, -180.0, -168.75}}, + {"q17", []float64{-37.96875, -36.5625, 94.21875, 95.625}}, + {"x9wzer", []float64{9.79431152344, 9.7998046875, 167.135009766, 167.145996094}}, + {"7xk2s7425n6", []float64{-4.1143463552, -4.1143450141, -16.3334485888, -16.3334472477}}, + {"jv", []float64{-61.875, -56.25, 78.75, 90.0}}, + {"u5juvw", []float64{62.7429199219, 62.7484130859, 8.32763671875, 8.33862304688}}, + {"cnuczt0c", []float64{83.3040046692, 83.3041763306, -127.989692688, -127.989349365}}, + {"7f3jv330zuh", []float64{-31.3259911537, -31.3259898126, -9.61132586002, -9.61132451892}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"70jqgb8uv", []float64{-43.8099145889, -43.8098716736, -37.4511480331, -37.4511051178}}, + {"u5t5d155", []float64{65.3087425232, 65.3089141846, 7.12326049805, 7.1236038208}}, + {"r9hy1gz8m4p", []float64{-38.2996594906, -38.2996581495, 164.267115444, 164.267116785}}, + {"qwmurb9920", []float64{-9.09371852875, -9.09371316433, 120.928573608, 120.928584337}}, + {"d2pt693fw4", []float64{0.930157899857, 0.930163264275, -68.0906009674, -68.0905902386}}, + {"zfgbx", []float64{60.556640625, 60.6005859375, 174.331054688, 174.375}}, + {"c7mtdqkfuuc", []float64{64.2828767002, 64.2828780413, -115.910019726, -115.910018384}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"w89eqd", []float64{3.39477539062, 3.40026855469, 114.895019531, 114.906005859}}, + {"75mtem68vx", []float64{-25.7229477167, -25.7229423523, -37.1191334724, -37.1191227436}}, + {"12e9fwr", []float64{-86.8455505371, -86.8441772461, -118.708648682, -118.707275391}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"nq", []float64{-56.25, -50.625, 101.25, 112.5}}, + {"svnx", []float64{29.35546875, 29.53125, 42.890625, 43.2421875}}, + {"0e1kd5pxdgt", []float64{-72.316198647, -72.3161973059, -155.64387247, -155.643871129}}, + {"pgk", []float64{-71.71875, -70.3125, 174.375, 175.78125}}, + {"vry9q", []float64{88.8134765625, 88.857421875, 65.654296875, 65.6982421875}}, + {"6f2x91u", []float64{-31.0157775879, -31.0144042969, -55.4974365234, -55.4960632324}}, + {"3hefw", []float64{-19.248046875, -19.2041015625, -129.462890625, -129.418945312}}, + {"g", []float64{45.0, 90.0, -45.0, 0.0}}, + {"m66erp", []float64{-31.7340087891, -31.728515625, 60.0732421875, 60.0842285156}}, + {"5nwny", []float64{-52.2509765625, -52.20703125, -36.298828125, -36.2548828125}}, + {"d5", []float64{16.875, 22.5, -90.0, -78.75}}, + {"wuphy8", []float64{23.3349609375, 23.3404541016, 133.879394531, 133.890380859}}, + {"b8u631kf", []float64{49.6214675903, 49.6216392517, -151.472969055, -151.472625732}}, + {"34d8j", []float64{-30.9375, -30.8935546875, -131.264648438, -131.220703125}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"he", []float64{-73.125, -67.5, 22.5, 33.75}}, + {"yec1yepjd", []float64{66.4187908173, 66.4188337326, 114.201593399, 114.201636314}}, + {"h0p58hpz", []float64{-89.3615913391, -89.3614196777, 9.85439300537, 9.85473632812}}, + {"8u", []float64{22.5, 28.125, -146.25, -135.0}}, + {"hg9sxf0mu", []float64{-69.509510994, -69.5094680786, 36.200466156, 36.2005090714}}, + {"7zdg4c9868", []float64{-2.27687358856, -2.27686822414, -7.25979566574, -7.2597849369}}, + {"1h", []float64{-67.5, -61.875, -135.0, -123.75}}, + {"k7h239zr4097", []float64{-28.0702368356, -28.070236668, 17.3025243357, 17.302524671}}, + {"9h", []float64{22.5, 28.125, -135.0, -123.75}}, + {"fx", []float64{84.375, 90.0, -67.5, -56.25}}, + {"sf66h05", []float64{13.0078125, 13.009185791, 37.093963623, 37.0953369141}}, + {"4ge39", []float64{-70.048828125, -70.0048828125, -51.6357421875, -51.591796875}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"ypd", []float64{87.1875, 88.59375, 92.8125, 94.21875}}, + {"gbzh2r", []float64{50.0042724609, 50.009765625, -1.39526367188, -1.38427734375}}, + {"h0", []float64{-90.0, -84.375, 0.0, 11.25}}, + {"nk7wkxwvku2", []float64{-64.952994436, -64.9529930949, 106.379102468, 106.37910381}}, + {"23qu", []float64{-37.265625, -37.08984375, -159.2578125, -158.90625}}, + {"n15b3", []float64{-84.3310546875, -84.287109375, 95.3173828125, 95.361328125}}, + {"8x9c6f1cjkn", []float64{42.4184060097, 42.4184073508, -154.915576279, -154.915574938}}, + {"5dr", []float64{-77.34375, -75.9375, -12.65625, -11.25}}, + {"022q4", []float64{-87.5390625, -87.4951171875, -168.310546875, -168.266601562}}, + {"52", []float64{-90.0, -84.375, -33.75, -22.5}}, + {"s0j8hb", []float64{0.0, 0.0054931640625, 7.94311523438, 7.9541015625}}, + {"58ygg9vn9nj", []float64{-85.1113092899, -85.1113079488, -12.8470878303, -12.8470864892}}, + {"ztzqs1", []float64{78.4918212891, 78.4973144531, 167.87109375, 167.882080078}}, + {"n5x", []float64{-70.3125, -68.90625, 99.84375, 101.25}}, + {"jh593g3fsf", []float64{-67.261980772, -67.2619754076, 50.001386404, 50.0013971329}}, + {"8vjg52364", []float64{28.6540603638, 28.6541032791, -138.01943779, -138.019394875}}, + {"dqeh1", []float64{37.265625, 37.3095703125, -74.4873046875, -74.443359375}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"m1mjx", []float64{-37.001953125, -36.9580078125, 52.3388671875, 52.3828125}}, + {"w4fr", []float64{16.69921875, 16.875, 93.1640625, 93.515625}}, + {"k2v0kj", []float64{-40.7098388672, -40.7043457031, 18.45703125, 18.4680175781}}, + {"ytmdb30u", []float64{75.0208282471, 75.0209999084, 120.246391296, 120.246734619}}, + {"wzuhv", []float64{44.4287109375, 44.47265625, 129.594726562, 129.638671875}}, + {"84m2ue733hx1", []float64{12.8061776049, 12.8061777726, -172.414918095, -172.41491776}}, + {"02", []float64{-90.0, -84.375, -168.75, -157.5}}, + {"x8j5vd68s", []float64{0.671625137329, 0.671668052673, 164.776554108, 164.776597023}}, + {"7k", []float64{-22.5, -16.875, -33.75, -22.5}}, + {"4xtrffmbtxr", []float64{-46.4377109706, -46.4377096295, -59.9881960452, -59.9881947041}}, + {"k8x", []float64{-42.1875, -40.78125, 32.34375, 33.75}}, + {"yyxsh", []float64{82.265625, 82.3095703125, 134.47265625, 134.516601562}}, + {"f2rqpzxu", []float64{47.502822876, 47.5029945374, -68.2034683228, -68.203125}}, + {"d6h4jg", []float64{11.6180419922, 11.6235351562, -72.8723144531, -72.861328125}}, + {"e7d", []float64{19.6875, 21.09375, -30.9375, -29.53125}}, + {"bbs0tcr5wc9", []float64{47.9078659415, 47.9078672826, -140.362410396, -140.362409055}}, + {"7fuf", []float64{-29.1796875, -29.00390625, -4.5703125, -4.21875}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"tt9p6y", []float64{32.2448730469, 32.2503662109, 69.0270996094, 69.0380859375}}, + {"xypsv42g6pr", []float64{34.5979173481, 34.5979186893, 179.517726749, 179.51772809}}, + {"p4gvh3sr97h", []float64{-73.6428004503, -73.6427991092, 140.466100574, 140.466101915}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"6ecy4rk", []float64{-22.8117370605, -22.8103637695, -64.9346923828, -64.9333190918}}, + {"045xe38uj", []float64{-77.4227142334, -77.4226713181, -174.934058189, -174.934015274}}, + {"xyunrc48", []float64{39.0728759766, 39.0730476379, 174.719009399, 174.719352722}}, + {"enm03", []float64{35.2001953125, 35.244140625, -37.9248046875, -37.880859375}}, + {"n7r9k6ecbhm", []float64{-71.4849673212, -71.4849659801, 111.988799125, 111.988800466}}, + {"2bpxnf2", []float64{-43.7571716309, -43.7557983398, -135.406494141, -135.40512085}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"vs65p7h9m", []float64{69.4502878189, 69.4503307343, 70.6374979019, 70.6375408173}}, + {"j6vt6yyjmms", []float64{-73.5703888535, -73.5703875124, 64.1136950254, 64.1136963665}}, + {"pp7z8s7", []float64{-47.8770446777, -47.8756713867, 140.299530029, 140.30090332}}, + {"skxsqyzdgn", []float64{26.0971534252, 26.0971587896, 22.103934288, 22.1039450169}}, + {"y1x7mdy", []float64{54.0238952637, 54.0252685547, 100.445251465, 100.446624756}}, + {"ck9n9vptne", []float64{71.4834183455, 71.4834237099, -122.256267071, -122.256256342}}, + {"fq7u12930mgt", []float64{80.862324927, 80.8623250946, -73.4198988229, -73.4198984876}}, + {"zs50j6", []float64{67.5109863281, 67.5164794922, 161.949462891, 161.960449219}}, + {"9jccmq0j", []float64{32.5972938538, 32.5974655151, -132.308349609, -132.308006287}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"hhtn7", []float64{-63.5888671875, -63.544921875, 7.1630859375, 7.20703125}}, + {"480spdnxu", []float64{-89.2845582962, -89.2845153809, -66.4581871033, -66.4581441879}}, + {"ghz87yh", []float64{71.7956542969, 71.7970275879, -34.2828369141, -34.281463623}}, + {"cf", []float64{56.25, 61.875, -101.25, -90.0}}, + {"gd1n5pnc8p7", []float64{57.3434360325, 57.3434373736, -20.9526403248, -20.9526389837}}, + {"hef9dvw3q", []float64{-68.6121511459, -68.6121082306, 26.1453151703, 26.1453580856}}, + {"wbjwhe8rkyf0", []float64{1.07519432902, 1.07519449666, 131.682678759, 131.682679094}}, + {"pndb3wg", []float64{-53.3564758301, -53.3551025391, 138.937225342, 138.938598633}}, + {"bh2k52ewybs", []float64{69.6132829785, 69.6132843196, -179.500513673, -179.500512332}}, + {"t4r", []float64{12.65625, 14.0625, 54.84375, 56.25}}, + {"s7215", []float64{18.45703125, 18.5009765625, 11.3818359375, 11.42578125}}, + {"u8", []float64{45.0, 50.625, 22.5, 33.75}}, + {"f2", []float64{45.0, 50.625, -78.75, -67.5}}, + {"3hwb5r0etbt", []float64{-19.6484443545, -19.6484430134, -125.36405012, -125.364048779}}, + {"wendsfh63", []float64{17.3258256912, 17.3258686066, 121.855244637, 121.855287552}}, + {"90n", []float64{0.0, 1.40625, -126.5625, -125.15625}}, + {"e5mx13zs4t2", []float64{19.5220465958, 19.5220479369, -37.2002863884, -37.2002850473}}, + {"6kngstqxn", []float64{-21.854724884, -21.8546819687, -69.0508747101, -69.0508317947}}, + {"rgs", []float64{-25.3125, -23.90625, 174.375, 175.78125}}, + {"sbd1n1z90", []float64{2.99806594849, 2.99810886383, 36.8364715576, 36.836514473}}, + {"693c7m26y6", []float64{-37.7197015285, -37.7196961641, -64.8956286907, -64.8956179619}}, + {"ynu", []float64{82.96875, 84.375, 95.625, 97.03125}}, + {"68t74uch2444", []float64{-41.6333230957, -41.6333229281, -59.9949619174, -59.9949615821}}, + {"jcmv2zscc", []float64{-82.0043992996, -82.0043563843, 86.875462532, 86.8755054474}}, + {"3c", []float64{-39.375, -33.75, -101.25, -90.0}}, + {"r76ymc", []float64{-25.6146240234, -25.6091308594, 150.369873047, 150.380859375}}, + {"kj9vj024cd", []float64{-13.1817376614, -13.1817322969, 2.68072843552, 2.68073916435}}, + {"scr", []float64{7.03125, 8.4375, 43.59375, 45.0}}, + {"57g8wvk", []float64{-68.7895202637, -68.7881469727, -28.5260009766, -28.5246276855}}, + {"8qde6", []float64{37.1337890625, 37.177734375, -165.146484375, -165.102539062}}, + {"7ve260", []float64{-14.0185546875, -14.0130615234, -6.591796875, -6.58081054688}}, + {"trcxzhy6", []float64{44.9824905396, 44.9826622009, 58.6755752563, 58.6759185791}}, + {"syqw7fyhx", []float64{36.2707614899, 36.2708044052, 43.0639600754, 43.0640029907}}, + {"tb0", []float64{0.0, 1.40625, 78.75, 80.15625}}, + {"dttugd1xtj5p", []float64{31.7847627215, 31.7847628891, -59.2579753697, -59.2579750344}}, + {"v899zsbyh7f2", []float64{48.1472598016, 48.1472599693, 69.9401802197, 69.940180555}}, + {"8e", []float64{16.875, 22.5, -157.5, -146.25}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"ny4017r8x", []float64{-56.2320613861, -56.2320184708, 126.628031731, 126.628074646}}, + {"8ued", []float64{25.6640625, 25.83984375, -141.328125, -140.9765625}}, + {"bqpsbj0h9p1t", []float64{79.6132376231, 79.6132377908, -158.203080073, -158.203079738}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"b5m", []float64{63.28125, 64.6875, -172.96875, -171.5625}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"w722c", []float64{18.4130859375, 18.45703125, 101.645507812, 101.689453125}}, + {"ey", []float64{33.75, 39.375, -11.25, 0.0}}, + {"6ndrutd", []float64{-7.04498291016, -7.04360961914, -86.6354370117, -86.6340637207}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"47jtykh", []float64{-72.0922851562, -72.0909118652, -70.7354736328, -70.7341003418}}, + {"krb5qvkb", []float64{-0.806121826172, -0.805950164795, 11.5531539917, 11.5534973145}}, + {"mtpgnes7uugs", []float64{-16.3277602941, -16.3277601264, 78.6901270598, 78.6901273951}}, + {"6062z", []float64{-43.4619140625, -43.41796875, -86.5283203125, -86.484375}}, + {"241quukp", []float64{-32.5389289856, -32.5387573242, -178.027954102, -178.027610779}}, + {"g4m49", []float64{58.095703125, 58.1396484375, -37.9248046875, -37.880859375}}, + {"wwq", []float64{35.15625, 36.5625, 120.9375, 122.34375}}, + {"beujb1c", []float64{67.1141052246, 67.1154785156, -151.873626709, -151.872253418}}, + {"h4", []float64{-78.75, -73.125, 0.0, 11.25}}, + {"69xq1n9v", []float64{-35.4712486267, -35.4710769653, -57.2583389282, -57.2579956055}}, + {"cjpu", []float64{73.828125, 74.00390625, -124.1015625, -123.75}}, + {"pmks2611hw", []float64{-59.7104895115, -59.7104841471, 152.590677738, 152.590688467}}, + {"zd78cuj300z8", []float64{57.8102342784, 57.8102344461, 162.505999133, 162.505999468}}, + {"g9h460p7719", []float64{51.0210737586, 51.0210750997, -16.777022928, -16.7770215869}}, + {"crncwf7g0c", []float64{84.6515518427, 84.6515572071, -113.955999613, -113.955988884}}, + {"mp4", []float64{-5.625, -4.21875, 47.8125, 49.21875}}, + {"3ghgz3gsjgr", []float64{-27.4555031955, -27.4555018544, -94.2466463149, -94.2466449738}}, + {"m2g9", []float64{-40.60546875, -40.4296875, 61.171875, 61.5234375}}, + {"ngt309w", []float64{-70.1284790039, -70.1271057129, 131.163024902, 131.164398193}}, + {"hgn9u3537p", []float64{-72.8116375208, -72.8116321564, 43.08198452, 43.0819952488}}, + {"6spnhu", []float64{-21.4233398438, -21.4178466797, -57.4475097656, -57.4365234375}}, + {"z22v4r0d82", []float64{47.3240375519, 47.3240429163, 147.404261827, 147.404272556}}, + {"ytypn", []float64{78.57421875, 78.6181640625, 121.201171875, 121.245117188}}, + {"qstc", []float64{-19.51171875, -19.3359375, 120.5859375, 120.9375}}, + {"y8r48c5", []float64{46.8511962891, 46.8525695801, 122.380828857, 122.382202148}}, + {"uq2xybqqg", []float64{81.5210866928, 81.5211296082, 12.2584676743, 12.2585105896}}, + {"95xm8dtz5u42", []float64{20.6692528725, 20.6692530401, -124.77465447, -124.774654135}}, + {"x0uqxwj", []float64{5.39428710938, 5.39566040039, 141.313018799, 141.31439209}}, + {"0kve9v", []float64{-62.6385498047, -62.6330566406, -160.938720703, -160.927734375}}, + {"szwv1er87", []float64{43.0843019485, 43.0843448639, 43.3185338974, 43.3185768127}}, + {"88zscymedp", []float64{5.08868157864, 5.08868694305, -146.868581772, -146.868571043}}, + {"pd", []float64{-78.75, -73.125, 157.5, 168.75}}, + {"g6", []float64{56.25, 61.875, -33.75, -22.5}}, + {"3pg6r201vv51", []float64{-1.01041479036, -1.01041462272, -130.110833198, -130.110832863}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"g02", []float64{46.40625, 47.8125, -45.0, -43.59375}}, + {"jx", []float64{-50.625, -45.0, 67.5, 78.75}}, + {"zksxs2nz2j3", []float64{71.6321320832, 71.6321334243, 152.774163634, 152.774164975}}, + {"erqyg8ff4d", []float64{41.9722473621, 41.9722527266, -24.1001200676, -24.1001093388}}, + {"n3wm1r3p4jev", []float64{-80.6425363384, -80.6425361708, 110.095458291, 110.095458627}}, + {"sqfbq13pjp9", []float64{38.0208036304, 38.0208049715, 15.3824485838, 15.3824499249}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"y0", []float64{45.0, 50.625, 90.0, 101.25}}, + {"04q3tpcc68bq", []float64{-77.0372864977, -77.03728633, -170.988700055, -170.988699719}}, + {"81", []float64{5.625, 11.25, -180.0, -168.75}}, + {"nj", []float64{-61.875, -56.25, 90.0, 101.25}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"8bxs0rfgp3n7", []float64{3.55871787295, 3.55871804059, -135.688042603, -135.688042268}}, + {"j0rm6", []float64{-87.6708984375, -87.626953125, 55.283203125, 55.3271484375}}, + {"86v9", []float64{15.64453125, 15.8203125, -161.015625, -160.6640625}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"nmcsv", []float64{-56.8212890625, -56.77734375, 103.579101562, 103.623046875}}, + {"cqytkn1d56", []float64{83.9249145985, 83.9249199629, -114.431394339, -114.43138361}}, + {"jfpcxxud86r9", []float64{-78.4433147125, -78.4433145449, 89.9842279404, 89.9842282757}}, + {"zf8cm8fkrg", []float64{59.2870920897, 59.2870974541, 170.049809217, 170.049819946}}, + {"98x", []float64{2.8125, 4.21875, -102.65625, -101.25}}, + {"e5mbzuu", []float64{18.4391784668, 18.4405517578, -36.5679931641, -36.566619873}}, + {"03g5kqkcp", []float64{-79.5504570007, -79.5504140854, -164.337658882, -164.337615967}}, + {"b362q0b79x2", []float64{52.0799548924, 52.0799562335, -165.321857929, -165.321856588}}, + {"m3pq", []float64{-38.3203125, -38.14453125, 66.4453125, 66.796875}}, + {"6564m2w5b0", []float64{-26.3198518753, -26.3198465109, -86.9485473633, -86.9485366344}}, + {"m9puw71wrf", []float64{-38.5664212704, -38.566415906, 78.6754882336, 78.6754989624}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"9t34u", []float64{30.0146484375, 30.05859375, -110.91796875, -110.874023438}}, + {"hewq3", []float64{-69.2138671875, -69.169921875, 31.3330078125, 31.376953125}}, + {"2m2uy1tgg0", []float64{-14.6249055862, -14.6249002218, -167.423615456, -167.423604727}}, + {"qu8dxgebd9wz", []float64{-19.22872575, -19.2287255824, 124.798967354, 124.798967689}}, + {"scwmj", []float64{9.31640625, 9.3603515625, 42.7587890625, 42.802734375}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"zm02460g", []float64{73.1365013123, 73.1366729736, 146.701469421, 146.701812744}}, + {"cgtd84ky", []float64{65.1403427124, 65.1405143738, -93.5091018677, -93.5087585449}}, + {"eq", []float64{33.75, 39.375, -33.75, -22.5}}, + {"ht26dn8h", []float64{-59.9929046631, -59.9927330017, 22.939453125, 22.9397964478}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"j70dzsu", []float64{-72.6155090332, -72.6141357422, 57.2882080078, 57.2895812988}}, + {"y241djby0dk", []float64{45.2962996066, 45.2963009477, 104.151447415, 104.151448756}}, + {"un9", []float64{81.5625, 82.96875, 1.40625, 2.8125}}, + {"2kzdj9", []float64{-17.9241943359, -17.9187011719, -157.961425781, -157.950439453}}, + {"m7b3gj70", []float64{-23.5697937012, -23.5696220398, 56.7375183105, 56.7378616333}}, + {"vvpmy3", []float64{74.1412353516, 74.1467285156, 89.2199707031, 89.2309570312}}, + {"ym0n0", []float64{74.1796875, 74.2236328125, 101.25, 101.293945312}}, + {"k6", []float64{-33.75, -28.125, 11.25, 22.5}}, + {"d96nj5sqmjfn", []float64{8.10626830906, 8.10626847669, -64.4617196918, -64.4617193565}}, + {"9yfrdwk", []float64{39.3214416504, 39.3228149414, -97.9705810547, -97.9692077637}}, + {"8vj712jd0kv", []float64{28.6527125537, 28.6527138948, -138.804685324, -138.804683983}}, + {"bk86yhd", []float64{70.8206176758, 70.8219909668, -168.132019043, -168.130645752}}, + {"6e3pb2s87q10", []float64{-25.3536236286, -25.353623461, -66.0764430463, -66.0764427111}}, + {"nxbn2n7yn", []float64{-45.2722549438, -45.2722120285, 112.505407333, 112.505450249}}, + {"cg4n", []float64{62.9296875, 63.10546875, -98.4375, -98.0859375}}, + {"4de6s37sk467", []float64{-75.4904382862, -75.4904381186, -62.7379387245, -62.7379383892}}, + {"pzegg", []float64{-47.1533203125, -47.109375, 174.155273438, 174.19921875}}, + {"xytyx62", []float64{37.7174377441, 37.7188110352, 177.154541016, 177.155914307}}, + {"2x4", []float64{-5.625, -4.21875, -154.6875, -153.28125}}, + {"j11m8j1eywcu", []float64{-83.3800566941, -83.3800565265, 46.7601537332, 46.7601540685}}, + {"k2xw57e", []float64{-41.1135864258, -41.1122131348, 21.9438171387, 21.9451904297}}, + {"2q74", []float64{-9.4921875, -9.31640625, -164.53125, -164.1796875}}, + {"9tp1960t8", []float64{28.4006023407, 28.400645256, -102.600631714, -102.600588799}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"2b2mwb", []float64{-42.626953125, -42.6214599609, -145.601806641, -145.590820312}}, + {"b7ts80s", []float64{65.481262207, 65.482635498, -161.010131836, -161.008758545}}, + {"10x0", []float64{-87.1875, -87.01171875, -125.15625, -124.8046875}}, + {"c5p9s65qqch", []float64{62.1507364511, 62.1507377923, -124.261599183, -124.261597842}}, + {"n1j", []float64{-84.375, -82.96875, 97.03125, 98.4375}}, + {"gjy9e2g7hcvc", []float64{77.6120662875, 77.6120664552, -35.7118779793, -35.7118776441}}, + {"d0nngw26hnc0", []float64{1.22123524547, 1.2212354131, -81.408175081, -81.4081747457}}, + {"sqqh99ewn", []float64{35.9565353394, 35.9565782547, 19.7584819794, 19.7585248947}}, + {"27", []float64{-28.125, -22.5, -168.75, -157.5}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"uv", []float64{73.125, 78.75, 33.75, 45.0}}, + {"nu6", []float64{-66.09375, -64.6875, 126.5625, 127.96875}}, + {"hjg9s97mjbfp", []float64{-57.3848481663, -57.3848479986, 5.12434154749, 5.12434188277}}, + {"cekd", []float64{63.6328125, 63.80859375, -106.171875, -105.8203125}}, + {"fx2s", []float64{86.484375, 86.66015625, -66.796875, -66.4453125}}, + {"zxx5n4wh37", []float64{87.7293223143, 87.7293276787, 167.615715265, 167.615725994}}, + {"k0redc", []float64{-42.9730224609, -42.9675292969, 10.6677246094, 10.6787109375}}, + {"xhj810f1r7", []float64{22.504350543, 22.5043559074, 142.781378031, 142.78138876}}, + {"9j", []float64{28.125, 33.75, -135.0, -123.75}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"z59900erg", []float64{64.8673582077, 64.867401123, 137.113966942, 137.114009857}}, + {"6dnccesgx8ts", []float64{-33.4225525707, -33.4225524031, -57.9350421578, -57.9350418225}}, + {"jx", []float64{-50.625, -45.0, 67.5, 78.75}}, + {"312y4y56", []float64{-36.8807601929, -36.8805885315, -133.819999695, -133.819656372}}, + {"32vk3g9np", []float64{-40.013923645, -40.0138807297, -116.288609505, -116.288566589}}, + {"kn39", []float64{-9.66796875, -9.4921875, 2.109375, 2.4609375}}, + {"eed4ybdn4mpp", []float64{20.1747029833, 20.174703151, -19.3880166113, -19.3880162761}}, + {"st", []float64{28.125, 33.75, 22.5, 33.75}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"0t8kw4xyhu5x", []float64{-58.2566988654, -58.2566986978, -156.873914078, -156.873913743}}, + {"dcy", []float64{9.84375, 11.25, -47.8125, -46.40625}}, + {"p3r", []float64{-82.96875, -81.5625, 156.09375, 157.5}}, + {"wdc0w9tbd6", []float64{15.5649769306, 15.564982295, 114.199887514, 114.199898243}}, + {"j3jc", []float64{-84.19921875, -84.0234375, 64.3359375, 64.6875}}, + {"k5cw3t", []float64{-22.7801513672, -22.7746582031, 2.17529296875, 2.18627929688}}, + {"pd6m", []float64{-76.46484375, -76.2890625, 160.6640625, 161.015625}}, + {"hnfqkjqrqnh", []float64{-50.9025013447, -50.9025000036, 3.34868967533, 3.34869101644}}, + {"6qkwd", []float64{-8.701171875, -8.6572265625, -72.333984375, -72.2900390625}}, + {"1q2x", []float64{-53.61328125, -53.4375, -123.046875, -122.6953125}}, + {"3nps1et4nde", []float64{-10.527292192, -10.5272908509, -124.380057603, -124.380056262}}, + {"gq2c8qnkx2", []float64{80.4536533356, 80.4536587, -32.6754319668, -32.6754212379}}, + {"q7d", []float64{-25.3125, -23.90625, 104.0625, 105.46875}}, + {"560", []float64{-78.75, -77.34375, -33.75, -32.34375}}, + {"j855ffmuxv4s", []float64{-89.3276607245, -89.3276605569, 71.8478319794, 71.8478323147}}, + {"sumfzt8z4", []float64{24.4210624695, 24.4211053848, 42.1666431427, 42.166686058}}, + {"bje9d2n0", []float64{76.201171875, 76.2013435364, -174.971008301, -174.970664978}}, + {"y943", []float64{50.80078125, 50.9765625, 115.6640625, 116.015625}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"gmn", []float64{73.125, 74.53125, -25.3125, -23.90625}}, + {"1djwe5s", []float64{-77.5881958008, -77.5868225098, -104.628295898, -104.626922607}}, + {"tb5wv3", []float64{1.19201660156, 1.19750976562, 83.9025878906, 83.9135742188}}, + {"58h3qnfv9m", []float64{-89.7422236204, -89.742218256, -16.2559354305, -16.2559247017}}, + {"f0du0sguugkg", []float64{48.5425508581, 48.5425510257, -86.1054797843, -86.105479449}}, + {"h6zyvwk", []float64{-73.3103942871, -73.3090209961, 22.3956298828, 22.3970031738}}, + {"vg2nu", []float64{64.4677734375, 64.51171875, 78.92578125, 78.9697265625}}, + {"j7r7de", []float64{-71.0870361328, -71.0815429688, 66.5551757812, 66.5661621094}}, + {"hjshn", []float64{-58.359375, -58.3154296875, 5.888671875, 5.9326171875}}, + {"46khrs1t", []float64{-76.5738487244, -76.573677063, -72.7933502197, -72.793006897}}, + {"g5p5p", []float64{62.40234375, 62.4462890625, -34.8486328125, -34.8046875}}, + {"7pxgy2sf", []float64{-2.15023040771, -2.15005874634, -33.8203811646, -33.8200378418}}, + {"vrz", []float64{88.59375, 90.0, 66.09375, 67.5}}, + {"bry3ngyuq", []float64{88.7908601761, 88.7909030914, -159.654779434, -159.654736519}}, + {"9cu9t1g3cs", []float64{10.1173567772, 10.1173621416, -94.6976208687, -94.6976101398}}, + {"82", []float64{0.0, 5.625, -168.75, -157.5}}, + {"m9yzmzsmxx", []float64{-33.8396555185, -33.8396501541, 77.2510313988, 77.2510421276}}, + {"4wv4wwxb2dr2", []float64{-51.5560363233, -51.5560361557, -60.1724312827, -60.1724309474}}, + {"j7ufy8", []float64{-68.4228515625, -68.4173583984, 63.2153320312, 63.2263183594}}, + {"hzrjzg9xjj", []float64{-48.1875532866, -48.1875479221, 43.9366006851, 43.936611414}}, + {"z5eh3ghtm7", []float64{65.4519671202, 65.4519724846, 139.302059412, 139.302070141}}, + {"6sjhet2cts", []float64{-21.6798663139, -21.6798609495, -60.3136754036, -60.3136646748}}, + {"vjyn6v", []float64{78.4698486328, 78.4753417969, 53.5583496094, 53.5693359375}}, + {"hysxqy94", []float64{-52.1270370483, -52.126865387, 40.3761291504, 40.3764724731}}, + {"8yd4qxm0d", []float64{36.9979190826, 36.997961998, -143.144903183, -143.144860268}}, + {"w43b42f", []float64{12.660369873, 12.6617431641, 92.5625610352, 92.5639343262}}, + {"7suc291x", []float64{-18.0548286438, -18.0546569824, -15.7962799072, -15.7959365845}}, + {"pq", []float64{-56.25, -50.625, 146.25, 157.5}}, + {"frd", []float64{87.1875, 88.59375, -75.9375, -74.53125}}, + {"r2", []float64{-45.0, -39.375, 146.25, 157.5}}, + {"5rs1x50fe5m", []float64{-47.531902045, -47.5319007039, -27.8162173927, -27.8162160516}}, + {"zjv30682s21k", []float64{77.5333506614, 77.533350829, 142.394326217, 142.394326553}}, + {"zbbtmw", []float64{50.1745605469, 50.1800537109, 169.694824219, 169.705810547}}, + {"e56k", []float64{18.984375, 19.16015625, -41.8359375, -41.484375}}, + {"v7dzp", []float64{65.91796875, 65.9619140625, 60.4248046875, 60.46875}}, + {"n2z8qt2hnzss", []float64{-85.707738027, -85.7077378593, 112.082815245, 112.08281558}}, + {"zbp8bt07bb5", []float64{45.159945488, 45.1599468291, 179.319227189, 179.31922853}}, + {"s551t", []float64{17.138671875, 17.1826171875, 4.4384765625, 4.482421875}}, + {"5zp7trjp", []float64{-49.9701118469, -49.9699401855, -0.817108154297, -0.816764831543}}, + {"81n2z7dz", []float64{5.77726364136, 5.77743530273, -170.888557434, -170.888214111}}, + {"dw", []float64{33.75, 39.375, -67.5, -56.25}}, + {"vvw35", []float64{76.11328125, 76.1572265625, 87.6708984375, 87.71484375}}, + {"zhtuc3b8bg5n", []float64{71.1572198197, 71.1572199874, 143.141591996, 143.141592331}}, + {"042w4qgx", []float64{-76.2507820129, -76.2506103516, -179.193191528, -179.192848206}}, + {"9sntb7", []float64{23.5272216797, 23.5327148438, -103.348388672, -103.337402344}}, + {"wt6x5nxk", []float64{30.7981109619, 30.7982826233, 116.157417297, 116.15776062}}, + {"spzwehn", []float64{44.7583007812, 44.7596740723, 10.6869506836, 10.6883239746}}, + {"hv70sbbpw64", []float64{-60.3754413128, -60.3754399717, 38.1777611375, 38.1777624786}}, + {"wxt", []float64{42.1875, 43.59375, 119.53125, 120.9375}}, + {"7dvqsskj8", []float64{-28.3643817902, -28.3643388748, -14.9139404297, -14.9138975143}}, + {"wm22", []float64{29.53125, 29.70703125, 101.6015625, 101.953125}}, + {"5t4kf", []float64{-61.0400390625, -60.99609375, -19.248046875, -19.2041015625}}, + {"5sy67z47fz1", []float64{-62.846608758, -62.8466074169, -13.542933315, -13.5429319739}}, + {"c36nw2msv0t", []float64{53.1760194898, 53.1760208309, -120.655067414, -120.655066073}}, + {"311v", []float64{-38.49609375, -38.3203125, -132.5390625, -132.1875}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"xcykeys", []float64{10.6704711914, 10.6718444824, 177.709350586, 177.710723877}}, + {"gtmvbbv", []float64{75.5461120605, 75.5474853516, -14.3742370605, -14.3728637695}}, + {"5n1", []float64{-56.25, -54.84375, -43.59375, -42.1875}}, + {"y08uf", []float64{48.6474609375, 48.69140625, 91.142578125, 91.1865234375}}, + {"ds4", []float64{22.5, 23.90625, -64.6875, -63.28125}}, + {"1t49n9", []float64{-61.6937255859, -61.6882324219, -108.698730469, -108.687744141}}, + {"v81d55x758", []float64{45.3713035583, 45.3713089228, 69.7513175011, 69.7513282299}}, + {"39j80vxmjh", []float64{-39.3439078331, -39.3439024687, -104.722495079, -104.72248435}}, + {"x3vd66k2vj46", []float64{10.251773335, 10.2517735027, 154.089306034, 154.089306369}}, + {"vg", []float64{61.875, 67.5, 78.75, 90.0}}, + {"06q04jnnuyz", []float64{-77.3150892556, -77.3150879145, -160.216156393, -160.216155052}}, + {"cvws", []float64{76.640625, 76.81640625, -92.109375, -91.7578125}}, + {"9d5j84gmgbv", []float64{12.2328941524, 12.2328954935, -108.276619166, -108.276617825}}, + {"mjg", []float64{-12.65625, -11.25, 49.21875, 50.625}}, + {"k7gspu", []float64{-23.1811523438, -23.1756591797, 16.5124511719, 16.5234375}}, + {"13nrmggfx", []float64{-83.0795574188, -83.0795145035, -114.702801704, -114.702758789}}, + {"ypj759", []float64{84.9078369141, 84.9133300781, 97.5366210938, 97.5476074219}}, + {"hhy3z0zjn", []float64{-62.9686546326, -62.9686117172, 9.10655021667, 9.10659313202}}, + {"b0xhjv", []float64{48.5430908203, 48.5485839844, -169.903564453, -169.892578125}}, + {"xucn7t7t11", []float64{27.8470855951, 27.8470909595, 170.314908028, 170.314918756}}, + {"f0yqwpw6", []float64{50.4028701782, 50.4030418396, -80.9386825562, -80.9383392334}}, + {"hcud164f7z", []float64{-79.7932773829, -79.7932720184, 40.1369941235, 40.1370048523}}, + {"k7b09t", []float64{-23.7908935547, -23.7854003906, 11.3159179688, 11.3269042969}}, + {"gzttr69", []float64{88.1240844727, 88.1254577637, -3.19564819336, -3.19427490234}}, + {"z1", []float64{50.625, 56.25, 135.0, 146.25}}, + {"mt", []float64{-16.875, -11.25, 67.5, 78.75}}, + {"vgpm3pe", []float64{62.839050293, 62.840423584, 88.9933776855, 88.9947509766}}, + {"xc2xk", []float64{8.3056640625, 8.349609375, 169.62890625, 169.672851562}}, + {"7gpegjrrn", []float64{-27.4357795715, -27.4357366562, -0.561075210571, -0.561032295227}}, + {"5m00s41bg21m", []float64{-61.7759934627, -61.775993295, -33.5716743395, -33.5716740042}}, + {"ybz9un", []float64{49.5593261719, 49.5648193359, 134.47265625, 134.483642578}}, + {"5cxhpu5jy6y", []float64{-80.8364005387, -80.8363991976, -1.06127768755, -1.06127634645}}, + {"0gk7412", []float64{-71.1845397949, -71.1831665039, -140.185546875, -140.184173584}}, + {"zj2", []float64{74.53125, 75.9375, 135.0, 136.40625}}, + {"6jj5vt6zv5", []float64{-16.1856347322, -16.1856293678, -82.7230596542, -82.7230489254}}, + {"mdp7x0cy8x", []float64{-33.1294924021, -33.1294870377, 78.0053544044, 78.0053651333}}, + {"515", []float64{-84.375, -82.96875, -40.78125, -39.375}}, + {"rcrpxqxje", []float64{-36.613740921, -36.6136980057, 178.922095299, 178.922138214}}, + {"ydd5n3qr6tu", []float64{59.5979855955, 59.5979869366, 115.595853925, 115.595855266}}, + {"hd", []float64{-78.75, -73.125, 22.5, 33.75}}, + {"rj4uc", []float64{-16.0400390625, -15.99609375, 138.911132812, 138.955078125}}, + {"0r3x", []float64{-47.98828125, -47.8125, -166.640625, -166.2890625}}, + {"490rp8g4y", []float64{-83.1399393082, -83.1398963928, -66.8144702911, -66.8144273758}}, + {"v19nuj", []float64{54.6514892578, 54.6569824219, 46.58203125, 46.5930175781}}, + {"wrg3hx7", []float64{43.8093566895, 43.8107299805, 106.022186279, 106.02355957}}, + {"ntzjry4", []float64{-56.7004394531, -56.6990661621, 122.687072754, 122.688446045}}, + {"3s5c85t2d", []float64{-22.2170162201, -22.2169733047, -107.219266891, -107.219223976}}, + {"wyy", []float64{37.96875, 39.375, 132.1875, 133.59375}}, + {"6wk17", []float64{-9.6240234375, -9.580078125, -61.7431640625, -61.69921875}}, + {"7m5wqccz7meg", []float64{-15.7654795982, -15.7654794306, -28.5289463773, -28.5289460421}}, + {"sk8n5z1qd2", []float64{26.4067554474, 26.4067608118, 11.4166080952, 11.416618824}}, + {"kw8hyjjj384", []float64{-7.57417201996, -7.57417067885, 22.7706053853, 22.7706067264}}, + {"14rw6bt3vx9v", []float64{-76.2420291267, -76.2420289591, -124.324827231, -124.324826896}}, + {"9cdu", []float64{9.140625, 9.31640625, -97.3828125, -97.03125}}, + {"0cptj3e6crgm", []float64{-83.4873395227, -83.4873393551, -135.467890911, -135.467890576}}, + {"0m", []float64{-61.875, -56.25, -168.75, -157.5}}, + {"mx", []float64{-5.625, 0.0, 67.5, 78.75}}, + {"p1sfuc", []float64{-81.0736083984, -81.0681152344, 141.888427734, 141.899414062}}, + {"03nduncm", []float64{-83.8536643982, -83.8534927368, -159.431877136, -159.431533813}}, + {"0m0j", []float64{-60.99609375, -60.8203125, -168.75, -168.3984375}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"fn", []float64{78.75, 84.375, -90.0, -78.75}}, + {"srddrb0", []float64{42.5830078125, 42.5843811035, 15.1062011719, 15.1075744629}}, + {"wpbf1dnj", []float64{43.957157135, 43.9573287964, 91.1288452148, 91.1291885376}}, + {"nv8k1f98", []float64{-58.3456420898, -58.3454704285, 124.180526733, 124.180870056}}, + {"7upj4ct1", []float64{-21.6126823425, -21.6125106812, -1.27853393555, -1.27819061279}}, + {"k312nzxpwm", []float64{-39.3324869871, -39.3324816227, 13.3143246174, 13.3143353462}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"qure08zn2n", []float64{-20.5611813068, -20.5611759424, 134.328460693, 134.328471422}}, + {"m1", []float64{-39.375, -33.75, 45.0, 56.25}}, + {"q7wbtv20e", []float64{-25.195684433, -25.1956415176, 110.995001793, 110.995044708}}, + {"fsqx0ywr3s", []float64{70.1736903191, 70.1736956835, -58.3177685738, -58.3177578449}}, + {"rn9k7k2927vd", []float64{-7.66684871167, -7.66684854403, 136.901339516, 136.901339851}}, + {"8ypg", []float64{34.27734375, 34.453125, -135.3515625, -135.0}}, + {"42ru", []float64{-87.890625, -87.71484375, -67.8515625, -67.5}}, + {"z3efhtnprhw", []float64{53.8177970052, 53.8177983463, 151.729739606, 151.729740947}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"fu", []float64{67.5, 73.125, -56.25, -45.0}}, + {"uz9", []float64{87.1875, 88.59375, 35.15625, 36.5625}}, + {"bqdnsr4y4", []float64{82.7445602417, 82.744603157, -165.746870041, -165.746827126}}, + {"rshpnngvd", []float64{-21.231508255, -21.2314653397, 163.393907547, 163.393950462}}, + {"4egtrvjfe", []float64{-67.9555034637, -67.9554605484, -62.2295236588, -62.2294807434}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"r2m2yc", []float64{-43.4564208984, -43.4509277344, 153.929443359, 153.940429688}}, + {"v9vps2z7", []float64{56.1667442322, 56.1669158936, 74.727973938, 74.7283172607}}, + {"dh", []float64{22.5, 28.125, -90.0, -78.75}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"7", []float64{-45.0, 0.0, -45.0, 0.0}}, + {"rrtp", []float64{-1.58203125, -1.40625, 153.28125, 153.6328125}}, + {"57wdet", []float64{-69.8455810547, -69.8400878906, -24.4555664062, -24.4445800781}}, + {"sxm3rvsc1x0y", []float64{41.031399183, 41.0313993506, 30.229977183, 30.2299775183}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"fmwp70q5", []float64{77.2138023376, 77.213973999, -70.1724243164, -70.1720809937}}, + {"vq3whey", []float64{81.2315368652, 81.2329101562, 58.5653686523, 58.5667419434}}, + {"vm55829xumn", []float64{73.7443381548, 73.7443394959, 60.4819867015, 60.4819880426}}, + {"pc", []float64{-84.375, -78.75, 168.75, 180.0}}, + {"76j", []float64{-33.75, -32.34375, -26.71875, -25.3125}}, + {"du5md", []float64{23.466796875, 23.5107421875, -51.591796875, -51.5478515625}}, + {"0b800r9", []float64{-87.1463012695, -87.1449279785, -146.237640381, -146.23626709}}, + {"r96suj", []float64{-37.1063232422, -37.1008300781, 161.19140625, 161.202392578}}, + {"dqn86", []float64{33.7939453125, 33.837890625, -69.521484375, -69.4775390625}}, + {"62jjysq7fun", []float64{-43.9652466774, -43.9652453363, -71.4243963361, -71.424394995}}, + {"s623s4p3v", []float64{12.9312086105, 12.9312515259, 11.7875146866, 11.7875576019}}, + {"j9w5", []float64{-81.03515625, -80.859375, 75.9375, 76.2890625}}, + {"cku2qh5ee64", []float64{71.7852795124, 71.7852808535, -117.504816949, -117.504815608}}, + {"ypmy864vvgs", []float64{86.9358202815, 86.9358216226, 98.1009525061, 98.1009538472}}, + {"kwe", []float64{-8.4375, -7.03125, 26.71875, 28.125}}, + {"gmq7083dvewj", []float64{75.0604587235, 75.0604588911, -24.9366608262, -24.9366604909}}, + {"9er", []float64{18.28125, 19.6875, -102.65625, -101.25}}, + {"5p89tmjs9j5", []float64{-47.5205630064, -47.5205616653, -44.0585620701, -44.058560729}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"ewy", []float64{37.96875, 39.375, -14.0625, -12.65625}}, + {"jtgef", []float64{-56.9970703125, -56.953125, 72.509765625, 72.5537109375}}, + {"9yjjw", []float64{34.716796875, 34.7607421875, -93.955078125, -93.9111328125}}, + {"926", []float64{1.40625, 2.8125, -120.9375, -119.53125}}, + {"bz1", []float64{84.375, 85.78125, -144.84375, -143.4375}}, + {"yjjpq0ecnve", []float64{74.4023618102, 74.4023631513, 97.3003654182, 97.3003667593}}, + {"w5e", []float64{19.6875, 21.09375, 94.21875, 95.625}}, + {"hqcn9wtcr", []float64{-50.8527517319, -50.8527088165, 12.7303647995, 12.7304077148}}, + {"qfh6xphngs", []float64{-33.2709145546, -33.2709091902, 130.039823055, 130.039833784}}, + {"1he586fypp", []float64{-64.0560919046, -64.0560865402, -130.766186714, -130.766175985}}, + {"4cc5sh9n3s", []float64{-79.5152020454, -79.515196681, -54.666531086, -54.6665203571}}, + {"9y5wfm", []float64{34.9639892578, 34.9694824219, -96.2292480469, -96.2182617188}}, + {"c97809", []float64{52.0367431641, 52.0422363281, -107.556152344, -107.545166016}}, + {"k9g2nkbm3j5h", []float64{-35.1292287558, -35.1292285882, 27.3453609645, 27.3453612998}}, + {"thdwugw196t", []float64{26.5185204148, 26.5185217559, 48.7326653302, 48.7326666713}}, + {"34nm41n89c8v", []float64{-32.8655058704, -32.8655057028, -126.114044376, -126.11404404}}, + {"buf7qgu", []float64{72.3106384277, 72.3120117188, -142.783813477, -142.782440186}}, + {"mhvh0u7f4", []float64{-17.55443573, -17.5543928146, 52.0694446564, 52.0694875717}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"f0vdwj1bu", []float64{49.6857976913, 49.6858406067, -81.9993782043, -81.999335289}}, + {"kcke59", []float64{-37.4359130859, -37.4304199219, 40.2319335938, 40.2429199219}}, + {"9rws4p0", []float64{42.9290771484, 42.9304504395, -114.521484375, -114.520111084}}, + {"fhj1u03epu", []float64{67.8095269203, 67.8095322847, -82.7905762196, -82.7905654907}}, + {"13296d9gwq1", []float64{-82.734657526, -82.7346561849, -122.934338897, -122.934337556}}, + {"4j", []float64{-61.875, -56.25, -90.0, -78.75}}, + {"gk5u1y2", []float64{68.2374572754, 68.2388305664, -28.3996582031, -28.3982849121}}, + {"9v6yrwx00", []float64{30.6655883789, 30.6656312943, -97.0436096191, -97.0435667038}}, + {"mc92", []float64{-36.5625, -36.38671875, 80.5078125, 80.859375}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"vtzr1we0jh5", []float64{78.6099457741, 78.6099471152, 77.7655689418, 77.7655702829}}, + {"ytmmrjr08p", []float64{75.4830640554, 75.4830694199, 120.200042725, 120.200053453}}, + {"y7q525c0mgkz", []float64{63.8731999509, 63.8732001185, 109.689126424, 109.68912676}}, + {"s5nc", []float64{17.05078125, 17.2265625, 9.4921875, 9.84375}}, + {"wk2", []float64{23.90625, 25.3125, 101.25, 102.65625}}, + {"f4beky4z04y", []float64{61.0742144287, 61.0742157698, -89.0843501687, -89.0843488276}}, + {"ywdu5yj95", []float64{82.2987556458, 82.2987985611, 116.539664268, 116.539707184}}, + {"n3", []float64{-84.375, -78.75, 101.25, 112.5}}, + {"0334vnb6", []float64{-82.4479293823, -82.4477577209, -167.123680115, -167.123336792}}, + {"xg65", []float64{18.80859375, 18.984375, 171.5625, 171.9140625}}, + {"0ebmse71br", []float64{-67.9212623835, -67.921257019, -156.946552992, -156.946542263}}, + {"ycwd9fc", []float64{53.8920593262, 53.8934326172, 132.968902588, 132.970275879}}, + {"0z2gsvd0tfzy", []float64{-48.573201634, -48.5732014664, -144.983568527, -144.983568192}}, + {"e041", []float64{0.17578125, 0.3515625, -42.1875, -41.8359375}}, + {"ntzdfpdcphj7", []float64{-57.1314592101, -57.1314590424, 123.138849624, 123.138849959}}, + {"jx1bfyyhgds1", []float64{-50.4552562349, -50.4552560672, 70.0901824236, 70.0901827589}}, + {"dhzuvhgspbew", []float64{27.5804938003, 27.580493968, -78.8766921312, -78.8766917959}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"teyjc173t", []float64{22.1116161346, 22.11165905, 75.986123085, 75.9861660004}}, + {"bg57uz4rxw5", []float64{62.5739514828, 62.5739528239, -141.467531472, -141.467530131}}, + {"52dtfpdc", []float64{-86.1353874207, -86.1352157593, -30.1427078247, -30.142364502}}, + {"vx1j39e", []float64{85.3060913086, 85.3074645996, 68.9762878418, 68.9776611328}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"psmpz5", []float64{-64.7149658203, -64.7094726562, 164.838867188, 164.849853516}}, + {"4xr95eeee", []float64{-49.023141861, -49.0230989456, -56.7943811417, -56.7943382263}}, + {"5j", []float64{-61.875, -56.25, -45.0, -33.75}}, + {"kpb", []float64{-1.40625, 0.0, 0.0, 1.40625}}, + {"dsub48epk", []float64{26.722741127, 26.7227840424, -60.7061576843, -60.706114769}}, + {"2urtnwtdw17", []float64{-20.1787023246, -20.1787009835, -135.409665853, -135.409664512}}, + {"e6s30gwjxm", []float64{14.2584782839, 14.2584836483, -27.7319276333, -27.7319169044}}, + {"qtx", []float64{-14.0625, -12.65625, 122.34375, 123.75}}, + {"qj0qvndweq3k", []float64{-15.651620999, -15.6516208313, 90.5748634413, 90.5748637766}}, + {"ffetyh28uyj", []float64{60.0967490673, 60.0967504084, -51.0635559261, -51.063554585}}, + {"z56t8nwqq7", []float64{64.2848414183, 64.2848467827, 138.52447629, 138.524487019}}, + {"7h", []float64{-22.5, -16.875, -45.0, -33.75}}, + {"9tuuw1pkyh", []float64{33.1410956383, 33.1411010027, -105.546426773, -105.546416044}}, + {"2m", []float64{-16.875, -11.25, -168.75, -157.5}}, + {"h7qt", []float64{-70.83984375, -70.6640625, 20.390625, 20.7421875}}, + {"t832ztb6psn", []float64{1.57003641129, 1.57003775239, 69.5880755782, 69.5880769193}}, + {"wk", []float64{22.5, 28.125, 101.25, 112.5}}, + {"ndjbb8w3n", []float64{-78.6152458191, -78.6152029037, 120.616750717, 120.616793633}}, + {"14pf3eqg4zd5", []float64{-78.3360836841, -78.3360835165, -124.026254117, -124.026253782}}, + {"9j", []float64{28.125, 33.75, -135.0, -123.75}}, + {"fr6ng34", []float64{86.9732666016, 86.9746398926, -75.7919311523, -75.7905578613}}, + {"p3ggurx2c", []float64{-79.455742836, -79.4556999207, 151.720204353, 151.720247269}}, + {"1h1pg1myn06", []float64{-66.1297975481, -66.129796207, -133.453757465, -133.453756124}}, + {"cqsue", []float64{82.353515625, 82.3974609375, -116.938476562, -116.89453125}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"s8jkw", []float64{0.791015625, 0.8349609375, 30.146484375, 30.1904296875}}, + {"67", []float64{-28.125, -22.5, -78.75, -67.5}}, + {"ywe4mn", []float64{81.9909667969, 81.9964599609, 116.938476562, 116.949462891}}, + {"0f5te71q9g", []float64{-77.7655917406, -77.7655863762, -141.183511019, -141.18350029}}, + {"v9s6tw70swwv", []float64{53.911406938, 53.9114071056, 73.7225837633, 73.7225840986}}, + {"0jbutv", []float64{-56.8377685547, -56.8322753906, -178.692626953, -178.681640625}}, + {"bn271bp", []float64{80.68359375, 80.684967041, -179.561920166, -179.560546875}}, + {"1vvyth", []float64{-56.4916992188, -56.4862060547, -92.9443359375, -92.9333496094}}, + {"7ruk94vup", []float64{-0.59944152832, -0.599398612976, -27.7212953568, -27.7212524414}}, + {"3hf", []float64{-18.28125, -16.875, -132.1875, -130.78125}}, + {"741rwgds3m4k", []float64{-32.4116574973, -32.4116573296, -42.9420667514, -42.9420664161}}, + {"2pye", []float64{-0.87890625, -0.703125, -170.859375, -170.5078125}}, + {"2", []float64{-45.0, 0.0, -180.0, -135.0}}, + {"e7", []float64{16.875, 22.5, -33.75, -22.5}}, + {"f", []float64{45.0, 90.0, -90.0, -45.0}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"4c5p5ke", []float64{-83.1198120117, -83.1184387207, -51.8843078613, -51.8829345703}}, + {"h7q", []float64{-71.71875, -70.3125, 19.6875, 21.09375}}, + {"4fjp8", []float64{-77.431640625, -77.3876953125, -49.21875, -49.1748046875}}, + {"p2cbvvvdt8", []float64{-85.6173992157, -85.6173938513, 148.971412182, 148.971422911}}, + {"xxjtqz46qmm", []float64{40.3367181122, 40.3367194533, 165.534370691, 165.534372032}}, + {"w1e", []float64{8.4375, 9.84375, 94.21875, 95.625}}, + {"fxpg4v3e", []float64{84.9316978455, 84.9318695068, -56.4786529541, -56.4783096313}}, + {"3be6u", []float64{-41.7041015625, -41.66015625, -96.50390625, -96.4599609375}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"seqqvkuphz", []float64{19.4951051474, 19.4951105118, 31.5254724026, 31.5254831314}}, + {"txy2t7xx", []float64{43.7020683289, 43.7022399902, 76.5300750732, 76.530418396}}, + {"s2hc2d2s", []float64{0.232772827148, 0.232944488525, 17.9523468018, 17.9526901245}}, + {"8zr0n4f62k", []float64{40.7967638969, 40.7967692614, -136.139477491, -136.139466763}}, + {"th1vxpfxnp9", []float64{23.5106107593, 23.5106121004, 47.7722467482, 47.7722480893}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"33", []float64{-39.375, -33.75, -123.75, -112.5}}, + {"gu", []float64{67.5, 73.125, -11.25, 0.0}}, + {"9vq49", []float64{29.970703125, 30.0146484375, -92.7685546875, -92.724609375}}, + {"tm", []float64{28.125, 33.75, 56.25, 67.5}}, + {"dpzw0p", []float64{44.6868896484, 44.6923828125, -79.453125, -79.4421386719}}, + {"gwg12", []float64{83.1884765625, 83.232421875, -18.28125, -18.2373046875}}, + {"b8vphv0m5k", []float64{50.4775643349, 50.4775696993, -150.259526968, -150.259516239}}, + {"pgpffhw1", []float64{-72.6167106628, -72.6165390015, 179.744567871, 179.744911194}}, + {"3r3w", []float64{-3.1640625, -2.98828125, -121.640625, -121.2890625}}, + {"u1d", []float64{53.4375, 54.84375, 2.8125, 4.21875}}, + {"mznb8v5xu6", []float64{-5.50830245018, -5.50829708576, 88.2801353931, 88.280146122}}, + {"8mb57vjrex4", []float64{32.9438298941, 32.9438312352, -168.577842414, -168.577841073}}, + {"zm", []float64{73.125, 78.75, 146.25, 157.5}}, + {"c9ef6tm74sg", []float64{53.8623873889, 53.86238873, -107.109378129, -107.109376788}}, + {"spww", []float64{43.2421875, 43.41796875, 9.140625, 9.4921875}}, + {"snp97n", []float64{34.0026855469, 34.0081787109, 10.6787109375, 10.6896972656}}, + {"zp9r6emsk8xx", []float64{88.4805002622, 88.4805004299, 136.875432059, 136.875432394}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"18zsh9bg", []float64{-85.0679969788, -85.0678253174, -101.754341125, -101.753997803}}, + {"v28", []float64{47.8125, 49.21875, 56.25, 57.65625}}, + {"4e", []float64{-73.125, -67.5, -67.5, -56.25}}, + {"evn0wp56", []float64{28.2516860962, 28.2518577576, -2.5443649292, -2.54402160645}}, + {"uyf9v", []float64{83.2763671875, 83.3203125, 37.4853515625, 37.529296875}}, + {"d7", []float64{16.875, 22.5, -78.75, -67.5}}, + {"05", []float64{-73.125, -67.5, -180.0, -168.75}}, + {"ujj8", []float64{73.125, 73.30078125, 7.734375, 8.0859375}}, + {"wcb7n8", []float64{10.37109375, 10.3765869141, 124.387207031, 124.398193359}}, + {"r35s2y4e2", []float64{-38.5944128036, -38.5943698883, 151.208267212, 151.208310127}}, + {"k", []float64{-45.0, 0.0, 0.0, 45.0}}, + {"8tm3h7b1f", []float64{29.7279310226, 29.727973938, -149.930334091, -149.930291176}}, + {"3xecw9gsguw3", []float64{-2.53837538883, -2.53837522119, -106.935942136, -106.9359418}}, + {"hqs10v", []float64{-53.2342529297, -53.2287597656, 16.9079589844, 16.9189453125}}, + {"b21g", []float64{45.52734375, 45.703125, -166.2890625, -165.9375}}, + {"vphhpnjt5b", []float64{85.1119422913, 85.1119476557, 50.9403312206, 50.9403419495}}, + {"kbd", []float64{-42.1875, -40.78125, 36.5625, 37.96875}}, + {"2c", []float64{-39.375, -33.75, -146.25, -135.0}}, + {"07ur", []float64{-67.67578125, -67.5, -162.7734375, -162.421875}}, + {"8e5ky1", []float64{17.7154541016, 17.7209472656, -152.666015625, -152.655029297}}, + {"k2w84t", []float64{-42.1600341797, -42.1545410156, 20.5004882812, 20.5114746094}}, + {"p9t4ncex81m", []float64{-81.2014035881, -81.201402247, 164.832694083, 164.832695425}}, + {"q67rduzsu6uz", []float64{-30.9984667785, -30.9984666109, 105.951650552, 105.951650888}}, + {"udwkypp0v", []float64{59.936041832, 59.9360847473, 31.5625619888, 31.5626049042}}, + {"pjsu1q9qg", []float64{-58.3225107193, -58.322467804, 141.7364645, 141.736507416}}, + {"2kj2w9b021b", []float64{-22.4024440348, -22.4024426937, -161.081542969, -161.081541628}}, + {"5k0", []float64{-67.5, -66.09375, -33.75, -32.34375}}, + {"t626vs8j", []float64{13.1652259827, 13.165397644, 56.8432617188, 56.8436050415}}, + {"hd0z4zr73", []float64{-77.4791479111, -77.4791049957, 23.6855363846, 23.6855792999}}, + {"79gjppfekhhk", []float64{-34.2341917008, -34.2341915332, -17.9700222239, -17.9700218886}}, + {"u9u", []float64{54.84375, 56.25, 28.125, 29.53125}}, + {"5zbfmj3n30", []float64{-45.9808301926, -45.9808248281, -9.97416973114, -9.9741590023}}, + {"1w1nt3g4t9pp", []float64{-55.0973731466, -55.0973729789, -110.858671814, -110.858671479}}, + {"f6bh910940", []float64{61.2654304504, 61.2654358149, -78.7052822113, -78.7052714825}}, + {"r65q38x", []float64{-32.6486206055, -32.6472473145, 150.895843506, 150.897216797}}, + {"xq2", []float64{35.15625, 36.5625, 146.25, 147.65625}}, + {"q87xbntvdv8d", []float64{-42.1947657689, -42.1947656013, 117.429890111, 117.429890446}}, + {"w1zhgmbpw", []float64{10.7115840912, 10.7116270065, 99.9868297577, 99.986872673}}, + {"5n", []float64{-56.25, -50.625, -45.0, -33.75}}, + {"9dz", []float64{15.46875, 16.875, -102.65625, -101.25}}, + {"n8r794hh15gv", []float64{-87.9668216966, -87.966821529, 122.744798921, 122.744799256}}, + {"px78re9", []float64{-49.1555786133, -49.1542053223, 162.752838135, 162.754211426}}, + {"3pps", []float64{-4.921875, -4.74609375, -124.453125, -124.1015625}}, + {"3s6um", []float64{-20.3466796875, -20.302734375, -108.413085938, -108.369140625}}, + {"9dj7zre6t7", []float64{11.9508236647, 11.9508290291, -104.793895483, -104.793884754}}, + {"4v1b", []float64{-61.875, -61.69921875, -53.7890625, -53.4375}}, + {"1k35z", []float64{-65.4345703125, -65.390625, -122.036132812, -121.9921875}}, + {"7z9n57", []float64{-1.74133300781, -1.73583984375, -9.70092773438, -9.68994140625}}, + {"3gzg", []float64{-23.37890625, -23.203125, -90.3515625, -90.0}}, + {"hy", []float64{-56.25, -50.625, 33.75, 45.0}}, + {"2rj6t", []float64{-5.185546875, -5.1416015625, -161.147460938, -161.103515625}}, + {"h", []float64{-90.0, -45.0, 0.0, 45.0}}, + {"dp44nc1t", []float64{39.7329139709, 39.7330856323, -86.8888092041, -86.8884658813}}, + {"0x1", []float64{-50.625, -49.21875, -156.09375, -154.6875}}, + {"dmwxxf", []float64{32.2668457031, 32.2723388672, -69.2687988281, -69.2578125}}, + {"khy29", []float64{-18.193359375, -18.1494140625, 8.8330078125, 8.876953125}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"phn4", []float64{-67.1484375, -66.97265625, 143.4375, 143.7890625}}, + {"qzhvp", []float64{-4.74609375, -4.7021484375, 130.737304688, 130.78125}}, + {"3n", []float64{-11.25, -5.625, -135.0, -123.75}}, + {"0nx", []float64{-53.4375, -52.03125, -170.15625, -168.75}}, + {"19uwx04h21", []float64{-79.0129369497, -79.0129315853, -105.86151123, -105.861500502}}, + {"7ur1q", []float64{-20.8740234375, -20.830078125, -1.142578125, -1.0986328125}}, + {"8yn6q9vmm", []float64{34.1560220718, 34.1560649872, -137.167868614, -137.167825699}}, + {"m4zk", []float64{-28.828125, -28.65234375, 55.1953125, 55.546875}}, + {"9bgzpypspd0", []float64{5.48287510872, 5.48287644982, -95.6253647804, -95.6253634393}}, + {"y1s", []float64{53.4375, 54.84375, 95.625, 97.03125}}, + {"qsyp207nvy", []float64{-17.0042717457, -17.0042663813, 120.941866636, 120.941877365}}, + {"rfb", []float64{-29.53125, -28.125, 168.75, 170.15625}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"5exm5p63", []float64{-69.3935966492, -69.3934249878, -12.1697616577, -12.169418335}}, + {"cnv22fdkruw", []float64{83.0271819234, 83.0271832645, -127.58079797, -127.580796629}}, + {"n7vg", []float64{-68.37890625, -68.203125, 109.3359375, 109.6875}}, + {"whvgd2h3sz9", []float64{27.3342821002, 27.3342834413, 98.1908561289, 98.19085747}}, + {"shbfuzk8vr", []float64{27.2421401739, 27.2421455383, 1.2698328495, 1.26984357834}}, + {"44vmk", []float64{-73.6083984375, -73.564453125, -82.44140625, -82.3974609375}}, + {"uhd1mfq", []float64{70.5445861816, 70.5459594727, 3.07342529297, 3.07479858398}}, + {"7bz", []float64{-40.78125, -39.375, -1.40625, 0.0}}, + {"h5b2wkdqpz", []float64{-68.7925726175, -68.7925672531, 0.629643201828, 0.629653930664}}, + {"h1", []float64{-84.375, -78.75, 0.0, 11.25}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"408bm1", []float64{-87.1380615234, -87.1325683594, -88.7255859375, -88.7145996094}}, + {"ggysyy5e2be6", []float64{66.9622308388, 66.9622310065, -1.80790107697, -1.8079007417}}, + {"w4u7dn8m9ndw", []float64{16.1206699535, 16.1206701212, 96.0648427159, 96.0648430511}}, + {"yq", []float64{78.75, 84.375, 101.25, 112.5}}, + {"2nwuht4w", []float64{-7.70587921143, -7.70570755005, -170.306625366, -170.306282043}}, + {"v5gqe", []float64{67.236328125, 67.2802734375, 49.7021484375, 49.74609375}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"cmehghzjm1", []float64{76.7994600534, 76.7994654179, -119.389586449, -119.38957572}}, + {"u207q361d", []float64{45.5784130096, 45.578455925, 11.8790531158, 11.8790960312}}, + {"n4pgq345pp", []float64{-78.1726652384, -78.172659874, 101.176142693, 101.176153421}}, + {"b2mn8", []float64{47.548828125, 47.5927734375, -161.71875, -161.674804688}}, + {"qbe6mp0g8et3", []float64{-41.7529202811, -41.7529201135, 128.541097529, 128.541097865}}, + {"sr04m", []float64{39.7705078125, 39.814453125, 11.4697265625, 11.513671875}}, + {"hfr0y7u988jx", []float64{-77.1910560317, -77.1910558641, 43.8746168464, 43.8746171817}}, + {"jrgxg5qkg7p", []float64{-45.0252610445, -45.0252597034, 61.3124428689, 61.3124442101}}, + {"gryut", []float64{89.384765625, 89.4287109375, -24.0380859375, -23.994140625}}, + {"14d5b766ppxg", []float64{-75.2600834705, -75.2600833029, -132.173112966, -132.173112631}}, + {"0ede2rgwt2", []float64{-69.6975231171, -69.6975177526, -153.968356848, -153.968346119}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"dj", []float64{28.125, 33.75, -90.0, -78.75}}, + {"xf", []float64{11.25, 16.875, 168.75, 180.0}}, + {"szf3p5k9rmx", []float64{43.7876281142, 43.7876294553, 37.228180021, 37.2281813622}}, + {"b9fqkhfem7", []float64{55.9690493345, 55.9690546989, -154.156497717, -154.156486988}}, + {"t7zw8x5c", []float64{22.2749519348, 22.2751235962, 66.8239974976, 66.8243408203}}, + {"f87dmh", []float64{46.8237304688, 46.8292236328, -62.3583984375, -62.3474121094}}, + {"yrd1swq12", []float64{87.4857187271, 87.4857616425, 104.268493652, 104.268536568}}, + {"s2", []float64{0.0, 5.625, 11.25, 22.5}}, + {"q9dhkgwy4kum", []float64{-35.7951473258, -35.7951471582, 115.530612208, 115.530612543}}, + {"7dr", []float64{-32.34375, -30.9375, -12.65625, -11.25}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"2rpk2ey43dw", []float64{-4.85693067312, -4.85692933202, -158.524402678, -158.524401337}}, + {"3wfeg", []float64{-6.3720703125, -6.328125, -108.852539062, -108.80859375}}, + {"ke5k4j", []float64{-27.3944091797, -27.3889160156, 27.158203125, 27.1691894531}}, + {"z0xq", []float64{48.8671875, 49.04296875, 145.1953125, 145.546875}}, + {"w1sy", []float64{9.4921875, 9.66796875, 96.6796875, 97.03125}}, + {"eqm14", []float64{35.33203125, 35.3759765625, -26.630859375, -26.5869140625}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"hjp9d9f", []float64{-61.6017150879, -61.6003417969, 10.6594848633, 10.6608581543}}, + {"p92v0", []float64{-82.08984375, -82.0458984375, 158.5546875, 158.598632812}}, + {"36m7g02m", []float64{-31.6823387146, -31.6821670532, -116.23500824, -116.234664917}}, + {"5g70e57zjrf", []float64{-71.6117633879, -71.6117620468, -6.89403623343, -6.89403489232}}, + {"65rkyfq4se", []float64{-25.8709841967, -25.8709788322, -79.4996237755, -79.4996130466}}, + {"eev1", []float64{21.26953125, 21.4453125, -15.46875, -15.1171875}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"m75926t", []float64{-27.8915405273, -27.8901672363, 61.1897277832, 61.1911010742}}, + {"1kjyeb", []float64{-66.357421875, -66.3519287109, -115.499267578, -115.48828125}}, + {"fb8rk2yfwmrp", []float64{49.0914924257, 49.0914925933, -55.7021225989, -55.7021222636}}, + {"y2qhd0j8x", []float64{47.1973514557, 47.197394371, 109.783244133, 109.783287048}}, + {"m2", []float64{-45.0, -39.375, 56.25, 67.5}}, + {"0543np5pgd23", []float64{-72.9094239883, -72.9094238207, -176.567995213, -176.567994878}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"d4h5zdhe5gy", []float64{11.9207011163, 11.9207024574, -84.0390613675, -84.0390600264}}, + {"9rcd", []float64{43.9453125, 44.12109375, -121.640625, -121.2890625}}, + {"ne9nrh75tq3", []float64{-69.1898868978, -69.1898855567, 114.218213707, 114.218215048}}, + {"7wk7", []float64{-9.31640625, -9.140625, -16.5234375, -16.171875}}, + {"995f97e", []float64{6.08367919922, 6.08505249023, -107.167510986, -107.166137695}}, + {"60kmung", []float64{-42.5459289551, -42.5445556641, -83.843536377, -83.8421630859}}, + {"845", []float64{11.25, 12.65625, -175.78125, -174.375}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"jehdxn0", []float64{-72.6525878906, -72.6512145996, 74.1357421875, 74.1371154785}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"1d", []float64{-78.75, -73.125, -112.5, -101.25}}, + {"rbjy", []float64{-43.9453125, -43.76953125, 176.8359375, 177.1875}}, + {"r8qgzf4r9uy", []float64{-42.9222710431, -42.922269702, 167.335936725, 167.335938066}}, + {"k5p", []float64{-28.125, -26.71875, 9.84375, 11.25}}, + {"f4z7", []float64{60.99609375, 61.171875, -79.8046875, -79.453125}}, + {"7rp35b", []float64{-5.44921875, -5.44372558594, -23.3898925781, -23.37890625}}, + {"zn71yyn0pbc", []float64{80.4968301952, 80.4968315363, 139.52395454, 139.523955882}}, + {"ppj7", []float64{-50.09765625, -49.921875, 142.3828125, 142.734375}}, + {"mqv3q", []float64{-6.8115234375, -6.767578125, 63.896484375, 63.9404296875}}, + {"tsdtmfq", []float64{26.2477111816, 26.2490844727, 71.276550293, 71.277923584}}, + {"72ey8b14uynx", []float64{-41.0444164462, -41.0444162786, -28.4420176595, -28.4420173243}}, + {"7qrgb", []float64{-9.1845703125, -9.140625, -22.8515625, -22.8076171875}}, + {"w7zmkdpezcm", []float64{22.0282383263, 22.0282396674, 111.653705388, 111.653706729}}, + {"kqwr1dh9jdbc", []float64{-7.19585834071, -7.19585817307, 20.1113973185, 20.1113976538}}, + {"kv9jx", []float64{-13.095703125, -13.0517578125, 35.4638671875, 35.5078125}}, + {"09", []float64{-84.375, -78.75, -157.5, -146.25}}, + {"f8ztmmp0", []float64{50.1690673828, 50.1692390442, -56.7127990723, -56.7124557495}}, + {"k5dj8cuwbxjg", []float64{-24.3348933198, -24.3348931521, 2.85166796297, 2.85166829824}}, + {"xd72j5qndwhn", []float64{12.6752517745, 12.6752519421, 162.298391461, 162.298391797}}, + {"esp42d", []float64{22.9064941406, 22.9119873047, -12.6342773438, -12.6232910156}}, + {"5sbfys", []float64{-62.7758789062, -62.7703857422, -21.1596679688, -21.1486816406}}, + {"8wsz02n", []float64{37.79296875, 37.794342041, -150.801086426, -150.799713135}}, + {"zeghw8", []float64{66.884765625, 66.8902587891, 162.004394531, 162.015380859}}, + {"u0xg7ug", []float64{48.4098815918, 48.4112548828, 11.0673522949, 11.0687255859}}, + {"0jb11", []float64{-57.48046875, -57.4365234375, -179.956054688, -179.912109375}}, + {"xv8cwtybm", []float64{31.2328004837, 31.232843399, 170.099816322, 170.099859238}}, + {"ef0cwqt7", []float64{11.5498924255, 11.5500640869, -9.91344451904, -9.91310119629}}, + {"hrh5k", []float64{-50.0537109375, -50.009765625, 17.05078125, 17.0947265625}}, + {"pnpdsx4eb", []float64{-55.7714509964, -55.7714080811, 145.748062134, 145.748105049}}, + {"8g2sx4gn", []float64{19.0884017944, 19.0885734558, -145.235137939, -145.234794617}}, + {"tsue3yr4z", []float64{27.3248434067, 27.324886322, 73.9149427414, 73.9149856567}}, + {"k4vq", []float64{-28.4765625, -28.30078125, 7.3828125, 7.734375}}, + {"mr1f1d430h", []float64{-5.26225805283, -5.26225268841, 58.7799453735, 58.7799561024}}, + {"dtuqkjybm", []float64{33.4740114212, 33.4740543365, -61.3381719589, -61.3381290436}}, + {"p00zpfbj5350", []float64{-88.7535613775, -88.7535612099, 136.39540717, 136.395407505}}, + {"n16jy8wrg38", []float64{-81.9539228082, -81.9539214671, 93.106867075, 93.1068684161}}, + {"3ckf9t6", []float64{-37.5004577637, -37.4990844727, -94.5016479492, -94.5002746582}}, + {"vvch78h7q7", []float64{78.0913943052, 78.0913996696, 80.3161633015, 80.3161740303}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"qkr8dj44", []float64{-20.9780502319, -20.9778785706, 111.887512207, 111.88785553}}, + {"s5dw7s", []float64{20.8081054688, 20.8135986328, 3.66943359375, 3.68041992188}}, + {"tpt", []float64{42.1875, 43.59375, 52.03125, 53.4375}}, + {"6vqn07ep", []float64{-14.3936347961, -14.3934631348, -47.7973937988, -47.7970504761}}, + {"7zbup2", []float64{-0.703125, -0.697631835938, -9.87670898438, -9.86572265625}}, + {"xd0j0wrn39f8", []float64{12.1643207967, 12.1643209644, 157.531653419, 157.531653754}}, + {"254kywz4", []float64{-27.2526168823, -27.2524452209, -176.540679932, -176.540336609}}, + {"6pkmr1875rp", []float64{-3.28710615635, -3.28710481524, -83.7153281271, -83.715326786}}, + {"69bmhmbw0de", []float64{-34.2447146773, -34.2447133362, -66.9609577954, -66.9609564543}}, + {"47jd", []float64{-72.7734375, -72.59765625, -71.015625, -70.6640625}}, + {"mw3ngtnj", []float64{-8.6289024353, -8.62873077393, 69.0682983398, 69.0686416626}}, + {"v", []float64{45.0, 90.0, 45.0, 90.0}}, + {"4uyq1", []float64{-62.2265625, -62.1826171875, -47.4169921875, -47.373046875}}, + {"9748v3e", []float64{17.0150756836, 17.0164489746, -119.999542236, -119.998168945}}, + {"sjy7", []float64{32.87109375, 33.046875, 8.7890625, 9.140625}}, + {"nc1jb2kb", []float64{-83.3628845215, -83.3627128601, 125.17375946, 125.174102783}}, + {"ffryw", []float64{58.798828125, 58.8427734375, -45.087890625, -45.0439453125}}, + {"3qfr7scg5s", []float64{-5.7302069664, -5.73020160198, -120.429575443, -120.429564714}}, + {"1x", []float64{-50.625, -45.0, -112.5, -101.25}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"n4wk", []float64{-75.234375, -75.05859375, 98.7890625, 99.140625}}, + {"bw2d2", []float64{80.5517578125, 80.595703125, -156.796875, -156.752929688}}, + {"ztgehr9mpt", []float64{77.9131776094, 77.9131829739, 162.610681057, 162.610691786}}, + {"bnkb", []float64{80.15625, 80.33203125, -173.3203125, -172.96875}}, + {"q0fmcn", []float64{-39.7375488281, -39.7320556641, 93.2080078125, 93.2189941406}}, + {"e0e1sxt9gwm", []float64{3.11770454049, 3.1177058816, -40.5757860839, -40.5757847428}}, + {"9qc", []float64{37.96875, 39.375, -122.34375, -120.9375}}, + {"0cybm9snr", []float64{-80.1029920578, -80.1029491425, -136.51031971, -136.510276794}}, + {"fp", []float64{84.375, 90.0, -90.0, -78.75}}, + {"7u69k7", []float64{-20.8575439453, -20.8520507812, -7.54760742188, -7.53662109375}}, + {"guh3mbvnwv7y", []float64{67.7249914035, 67.7249915712, -5.01359079033, -5.01359045506}}, + {"vgw4wgnrd58e", []float64{65.1447393559, 65.1447395235, 87.4928004295, 87.4928007647}}, + {"rzk732w", []float64{-3.64471435547, -3.64334106445, 174.789733887, 174.791107178}}, + {"kf", []float64{-33.75, -28.125, 33.75, 45.0}}, + {"rcfr28t0", []float64{-33.8790893555, -33.8789176941, 171.942901611, 171.943244934}}, + {"5bqnms", []float64{-87.4731445312, -87.4676513672, -2.57080078125, -2.55981445312}}, + {"fs84w", []float64{70.751953125, 70.7958984375, -67.236328125, -67.1923828125}}, + {"mcjrsmx", []float64{-38.0264282227, -38.0250549316, 86.3291931152, 86.3305664062}}, + {"u84", []float64{45.0, 46.40625, 25.3125, 26.71875}}, + {"gkv4g14m", []float64{72.2084999084, 72.2086715698, -26.5838241577, -26.583480835}}, + {"27dhxu", []float64{-24.4995117188, -24.4940185547, -165.596923828, -165.5859375}}, + {"0v", []float64{-61.875, -56.25, -146.25, -135.0}}, + {"bpurn", []float64{89.82421875, 89.8681640625, -173.759765625, -173.715820312}}, + {"p5", []float64{-73.125, -67.5, 135.0, 146.25}}, + {"f3ffsuh", []float64{55.3051757812, 55.3065490723, -74.6685791016, -74.6672058105}}, + {"j0zbr0tb", []float64{-85.7345581055, -85.7343864441, 56.2139511108, 56.2142944336}}, + {"vyz", []float64{82.96875, 84.375, 88.59375, 90.0}}, + {"082p96b5ey", []float64{-87.2596514225, -87.2596460581, -157.444907427, -157.444896698}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"g030qs", []float64{46.4721679688, 46.4776611328, -43.3081054688, -43.2971191406}}, + {"54", []float64{-78.75, -73.125, -45.0, -33.75}}, + {"fp5rcptn2gc", []float64{85.7795964181, 85.7795977592, -85.3788422048, -85.3788408637}}, + {"dk8z85s6516h", []float64{26.650436148, 26.6504363157, -77.6893445849, -77.6893442497}}, + {"3v1ebh18qujh", []float64{-16.1937826127, -16.193782445, -99.1382686794, -99.1382683441}}, + {"un50j3xf9", []float64{78.7586688995, 78.7587118149, 4.46014881134, 4.46019172668}}, + {"4b8y3gh", []float64{-86.0723876953, -86.0710144043, -55.1129150391, -55.111541748}}, + {"efdgh", []float64{14.58984375, 14.6337890625, -7.20703125, -7.1630859375}}, + {"1xxuk2", []float64{-47.0654296875, -47.0599365234, -101.414794922, -101.403808594}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"1z6", []float64{-49.21875, -47.8125, -98.4375, -97.03125}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"mxkjr1jdu5ru", []float64{-3.28991509974, -3.2899149321, 73.440352343, 73.4403526783}}, + {"x", []float64{0.0, 45.0, 135.0, 180.0}}, + {"3kpcn3sm", []float64{-22.315120697, -22.3149490356, -112.57106781, -112.570724487}}, + {"buk3t0ctrmt0", []float64{69.1749724746, 69.1749726422, -140.051333159, -140.051332824}}, + {"pp", []float64{-50.625, -45.0, 135.0, 146.25}}, + {"4h", []float64{-67.5, -61.875, -90.0, -78.75}}, + {"fjw1kcg4", []float64{76.1671829224, 76.1673545837, -81.3496398926, -81.3492965698}}, + {"877wsvjfz5", []float64{19.4517821074, 19.4517874718, -163.611187935, -163.611177206}}, + {"ru3", []float64{-21.09375, -19.6875, 170.15625, 171.5625}}, + {"yr", []float64{84.375, 90.0, 101.25, 112.5}}, + {"cu5x6cxq", []float64{68.7836837769, 68.7838554382, -96.1973190308, -96.196975708}}, + {"w04vuf4bdzjm", []float64{1.02185273543, 1.02185290307, 94.0798293427, 94.0798296779}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"4zdcmmp2p4", []float64{-47.5652968884, -47.5652915239, -52.1418428421, -52.1418321133}}, + {"eft02s1hu", []float64{14.1292333603, 14.1292762756, -4.19523239136, -4.19518947601}}, + {"zk4v9qdeg5q1", []float64{68.5031637736, 68.5031639412, 150.175689161, 150.175689496}}, + {"8xr", []float64{40.78125, 42.1875, -147.65625, -146.25}}, + {"3pxyrt", []float64{-1.68640136719, -1.68090820312, -123.771972656, -123.760986328}}, + {"cmh39xszs8", []float64{73.4311580658, 73.4311634302, -117.70080328, -117.700792551}}, + {"xrm9d0wb48", []float64{41.047668457, 41.0476738214, 154.081642628, 154.081653357}}, + {"d4bh0k", []float64{16.1938476562, 16.1993408203, -89.9890136719, -89.9780273438}}, + {"hk8", []float64{-64.6875, -63.28125, 11.25, 12.65625}}, + {"9hxqk54m0wn", []float64{26.4285027981, 26.4285041392, -124.625786841, -124.6257855}}, + {"mygnv0", []float64{-5.8447265625, -5.83923339844, 83.1884765625, 83.1994628906}}, + {"yrjmvs", []float64{85.4077148438, 85.4132080078, 108.874511719, 108.885498047}}, + {"52csyemvf12", []float64{-84.9274425209, -84.9274411798, -31.3469982147, -31.3469968736}}, + {"4jrvjj", []float64{-59.5623779297, -59.5568847656, -78.8818359375, -78.8708496094}}, + {"ys1", []float64{67.5, 68.90625, 113.90625, 115.3125}}, + {"unf91", []float64{83.14453125, 83.1884765625, 3.5595703125, 3.603515625}}, + {"h5che0vnt", []float64{-68.109998703, -68.1099557877, 1.5451669693, 1.54520988464}}, + {"ugrk3", []float64{64.0283203125, 64.072265625, 43.9892578125, 44.033203125}}, + {"9ush8c", []float64{26.1090087891, 26.1145019531, -95.5920410156, -95.5810546875}}, + {"q92pzb", []float64{-36.6064453125, -36.6009521484, 112.840576172, 112.8515625}}, + {"0e", []float64{-73.125, -67.5, -157.5, -146.25}}, + {"dbt1mchu", []float64{3.03840637207, 3.03857803345, -48.9595413208, -48.959197998}}, + {"98xv2m", []float64{3.76281738281, 3.76831054688, -101.590576172, -101.579589844}}, + {"rqd8u195kgu", []float64{-8.29684630036, -8.29684495926, 149.942988753, 149.942990094}}, + {"504wk58ccv", []float64{-88.8818138838, -88.8818085194, -41.3074886799, -41.307477951}}, + {"0dzjhbn", []float64{-73.65234375, -73.650970459, -147.43927002, -147.437896729}}, + {"sgcn", []float64{22.1484375, 22.32421875, 35.15625, 35.5078125}}, + {"46k78jw0x65w", []float64{-76.6982056573, -76.6982054897, -72.7648819238, -72.7648815885}}, + {"6w2cbxx3nf9", []float64{-9.49474900961, -9.4947476685, -66.4130924642, -66.4130911231}}, + {"zxmf4", []float64{86.1328125, 86.1767578125, 165.673828125, 165.717773438}}, + {"unf", []float64{82.96875, 84.375, 2.8125, 4.21875}}, + {"m4p", []float64{-33.75, -32.34375, 54.84375, 56.25}}, + {"dsc1rqss2w", []float64{26.9749438763, 26.9749492407, -65.7689452171, -65.7689344883}}, + {"cxp", []float64{84.375, 85.78125, -102.65625, -101.25}}, + {"zmh", []float64{73.125, 74.53125, 151.875, 153.28125}}, + {"tynvnjc8hdb", []float64{34.6605066955, 34.6605080366, 88.5081124306, 88.5081137717}}, + {"uk8hb", []float64{71.1474609375, 71.19140625, 11.25, 11.2939453125}}, + {"34d", []float64{-30.9375, -29.53125, -132.1875, -130.78125}}, + {"ts39vet4rzw5", []float64{24.2335202359, 24.2335204035, 69.8582813144, 69.8582816496}}, + {"3rt1fx5", []float64{-2.46643066406, -2.46505737305, -116.604766846, -116.603393555}}, + {"ujn8yhfpg", []float64{73.2842588425, 73.2843017578, 9.40717220306, 9.40721511841}}, + {"pdbvhzj", []float64{-73.6138916016, -73.6125183105, 158.770294189, 158.77166748}}, + {"q35", []float64{-39.375, -37.96875, 105.46875, 106.875}}, + {"szh5424hc", []float64{39.9031591415, 39.9032020569, 39.4766664505, 39.4767093658}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"tt1wjkr44e", []float64{29.2033928633, 29.2033982277, 69.8498082161, 69.8498189449}}, + {"1u3hdkn", []float64{-65.2807617188, -65.2793884277, -99.7366333008, -99.7352600098}}, + {"jc9", []float64{-81.5625, -80.15625, 80.15625, 81.5625}}, + {"627pp", []float64{-42.36328125, -42.3193359375, -74.2236328125, -74.1796875}}, + {"g46wqb4z", []float64{58.7560844421, 58.7562561035, -41.1839675903, -41.1836242676}}, + {"2407674", []float64{-33.1622314453, -33.1608581543, -179.546813965, -179.545440674}}, + {"3vbsrcxu", []float64{-11.9002532959, -11.9000816345, -100.195655823, -100.1953125}}, + {"u0mr9fpy", []float64{47.7366256714, 47.7367973328, 7.47035980225, 7.470703125}}, + {"p1s1", []float64{-81.38671875, -81.2109375, 140.625, 140.9765625}}, + {"ce7y6s1ugjpu", []float64{64.4026983529, 64.4026985206, -107.11415682, -107.114156485}}, + {"tujn", []float64{23.5546875, 23.73046875, 85.78125, 86.1328125}}, + {"fes", []float64{64.6875, 66.09375, -61.875, -60.46875}}, + {"28te871t29y", []float64{-41.5548755229, -41.5548741817, -149.752549231, -149.75254789}}, + {"2z9j0591", []float64{-1.9141960144, -1.91402435303, -144.842376709, -144.842033386}}, + {"e", []float64{0.0, 45.0, -45.0, 0.0}}, + {"90", []float64{0.0, 5.625, -135.0, -123.75}}, + {"jbfm12r", []float64{-84.900970459, -84.899597168, 81.9786071777, 81.9799804688}}, + {"y0ws", []float64{48.515625, 48.69140625, 99.140625, 99.4921875}}, + {"m2", []float64{-45.0, -39.375, 56.25, 67.5}}, + {"gpspv95sz", []float64{88.5561132431, 88.5561561584, -39.1281938553, -39.1281509399}}, + {"7k8u95cyjdx6", []float64{-18.8748412952, -18.8748411275, -32.6487181708, -32.6487178355}}, + {"c1fe0r", []float64{55.4095458984, 55.4150390625, -131.473388672, -131.462402344}}, + {"668wjecj2d", []float64{-29.8613011837, -29.8612958193, -77.8037810326, -77.8037703037}}, + {"dnq3", []float64{35.33203125, 35.5078125, -81.2109375, -80.859375}}, + {"m3sxdxnvmrr", []float64{-35.2047483623, -35.2047470212, 62.6974926889, 62.69749403}}, + {"zz3qpfvqzu", []float64{86.8522238731, 86.8522292376, 170.855931044, 170.855941772}}, + {"98mjjx8bu", []float64{2.3264837265, 2.32652664185, -105.225849152, -105.225806236}}, + {"pkmusy0e4j35", []float64{-65.2692317404, -65.2692315727, 154.545451552, 154.545451887}}, + {"j3f9dtm5r5n", []float64{-79.8631650209, -79.8631636798, 59.8826631904, 59.8826645315}}, + {"67up3c0uh9jn", []float64{-22.6256497577, -22.62564959, -73.0468659103, -73.046865575}}, + {"6q0fd9wn2", []float64{-10.8012342453, -10.80119133, -77.5772094727, -77.5771665573}}, + {"t82e5zrs", []float64{1.97410583496, 1.97427749634, 68.3782196045, 68.3785629272}}, + {"0hstxh", []float64{-63.6987304688, -63.6932373047, -173.364257812, -173.353271484}}, + {"qe1egcuetqe", []float64{-27.4555715919, -27.4555702507, 114.78057906, 114.780580401}}, + {"yhp25wc4v", []float64{67.5375509262, 67.5375938416, 100.350708961, 100.350751877}}, + {"z6uvby2nrt4k", []float64{61.5149248391, 61.5149250068, 152.962971367, 152.962971702}}, + {"29sd0863cx", []float64{-36.2092262506, -36.2092208862, -151.146748066, -151.146737337}}, + {"kvnx614", []float64{-15.5950927734, -15.5937194824, 42.981262207, 42.982635498}}, + {"mu1srk07", []float64{-21.7304420471, -21.7302703857, 81.1783218384, 81.1786651611}}, + {"5bz5bmq", []float64{-85.0932312012, -85.0918579102, -1.38702392578, -1.38565063477}}, + {"fu4yx9fr8gtk", []float64{68.6534980685, 68.6534982361, -52.0500935242, -52.0500931889}}, + {"3hyhj92rn", []float64{-17.5700569153, -17.5700139999, -126.320199966, -126.320157051}}, + {"345nw", []float64{-32.607421875, -32.5634765625, -130.517578125, -130.473632812}}, + {"q5f2p327mhy", []float64{-23.8988001645, -23.8987988234, 93.4832319617, 93.4832333028}}, + {"0wmufb9", []float64{-54.0060424805, -54.0046691895, -149.2918396, -149.290466309}}, + {"r", []float64{-45.0, 0.0, 135.0, 180.0}}, + {"07d2sde", []float64{-70.2108764648, -70.2095031738, -165.384063721, -165.38269043}}, + {"d0r2", []float64{1.40625, 1.58203125, -79.8046875, -79.453125}}, + {"znegsexfs23h", []float64{82.1973916143, 82.197391782, 140.482018143, 140.482018478}}, + {"sfr69qxg", []float64{13.1319236755, 13.1320953369, 44.010887146, 44.0112304688}}, + {"tr44b8brc", []float64{39.8638486862, 39.8638916016, 59.0848588943, 59.0849018097}}, + {"tbnqctecsf", []float64{1.21700406075, 1.21700942516, 87.6103341579, 87.6103448868}}, + {"jpfy538qu", []float64{-45.3421640396, -45.3421211243, 49.0105247498, 49.0105676651}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"gskrg0z5e", []float64{70.2732753754, 70.2733182907, -16.3818597794, -16.381816864}}, + {"6cz", []float64{-35.15625, -33.75, -46.40625, -45.0}}, + {"u67hm7b47423", []float64{58.4243181534, 58.424318321, 15.6995919719, 15.6995923072}}, + {"j154zhnnkyt3", []float64{-83.8685209863, -83.8685208187, 49.5348178223, 49.5348181576}}, + {"muqdpev4smv", []float64{-20.7211281359, -20.7211267948, 88.2272703946, 88.2272717357}}, + {"47h3upynmsru", []float64{-72.7737144381, -72.7737142704, -72.589170076, -72.5891697407}}, + {"g6j200", []float64{56.25, 56.2554931641, -26.3671875, -26.3562011719}}, + {"tw", []float64{33.75, 39.375, 67.5, 78.75}}, + {"c0pjhr520q", []float64{45.9173905849, 45.9173959494, -124.965008497, -124.964997768}}, + {"8nx", []float64{36.5625, 37.96875, -170.15625, -168.75}}, + {"47b2wvtns", []float64{-68.7870311737, -68.7869882584, -78.0947685242, -78.0947256088}}, + {"vrsbq", []float64{87.2314453125, 87.275390625, 63.193359375, 63.2373046875}}, + {"sz", []float64{39.375, 45.0, 33.75, 45.0}}, + {"xe61b0bnw", []float64{18.5941028595, 18.5941457748, 160.312757492, 160.312800407}}, + {"dky6qedz3w", []float64{27.1347606182, 27.1347659826, -69.6714520454, -69.6714413166}}, + {"vmvkqx2hb", []float64{78.1314611435, 78.1315040588, 63.9184570312, 63.9184999466}}, + {"t96m49xgr1y", []float64{7.9189632833, 7.9189646244, 70.7848772407, 70.7848785818}}, + {"brw2urrqcfex", []float64{87.3603346758, 87.3603348434, -159.764133766, -159.764133431}}, + {"z7m8", []float64{63.28125, 63.45703125, 153.984375, 154.3359375}}, + {"wm6w7f38", []float64{30.6422424316, 30.642414093, 104.932479858, 104.932823181}}, + {"rxj23rtt4y", []float64{-5.53896546364, -5.53896009922, 164.945415258, 164.945425987}}, + {"sfr9xsyzfn", []float64{12.9473769665, 12.9473823309, 44.6358203888, 44.6358311176}}, + {"9ubf9uq02e", []float64{27.1816080809, 27.1816134453, -100.110146999, -100.110136271}}, + {"kj25zp1gb6j", []float64{-14.7704637051, -14.770462364, 0.310037881136, 0.31003922224}}, + {"x4f", []float64{15.46875, 16.875, 137.8125, 139.21875}}, + {"xnn27kkf4c", []float64{33.8176399469, 33.8176453114, 143.938525915, 143.938536644}}, + {"61bhs9byn4", []float64{-34.3545806408, -34.3545752764, -89.8009586334, -89.8009479046}}, + {"rv2sve92mngr", []float64{-14.6144826896, -14.614482522, 169.696759768, 169.696760103}}, + {"zkvq2w", []float64{72.8503417969, 72.8558349609, 153.654785156, 153.665771484}}, + {"qprmp68h7kcd", []float64{-3.32535546273, -3.32535529509, 100.514057502, 100.514057837}}, + {"77pmzubu", []float64{-27.0874786377, -27.0873069763, -23.2130813599, -23.2127380371}}, + {"q73t2sumh3b", []float64{-25.7689382136, -25.7689368725, 103.387366533, 103.387367874}}, + {"3kxch9c", []float64{-19.5021057129, -19.5007324219, -112.652435303, -112.651062012}}, + {"t", []float64{0.0, 45.0, 45.0, 90.0}}, + {"3um1y618chw", []float64{-20.7749935985, -20.7749922574, -93.9419808984, -93.9419795573}}, + {"45nj7sxww", []float64{-72.1763134003, -72.1762704849, -81.3981342316, -81.3980913162}}, + {"rnkyjdv404", []float64{-8.77360224724, -8.77359688282, 141.928253174, 141.928263903}}, + {"p3", []float64{-84.375, -78.75, 146.25, 157.5}}, + {"sxbz", []float64{44.82421875, 45.0, 23.5546875, 23.90625}}, + {"xuj2k", []float64{22.5439453125, 22.587890625, 176.30859375, 176.352539062}}, + {"yhp9", []float64{67.67578125, 67.8515625, 100.546875, 100.8984375}}, + {"1yq4", []float64{-54.4921875, -54.31640625, -92.8125, -92.4609375}}, + {"u4m2jkw", []float64{57.6809692383, 57.6823425293, 7.62176513672, 7.62313842773}}, + {"xb9", []float64{2.8125, 4.21875, 170.15625, 171.5625}}, + {"ebf4e478jp", []float64{4.67060029507, 4.67060565948, -8.30064296722, -8.30063223839}}, + {"y7venx9", []float64{66.6622924805, 66.6636657715, 109.271392822, 109.272766113}}, + {"8qu", []float64{37.96875, 39.375, -163.125, -161.71875}}, + {"jw2jbzms66", []float64{-53.7924420834, -53.7924367189, 67.5406086445, 67.5406193733}}, + {"n", []float64{-90.0, -45.0, 90.0, 135.0}}, + {"jbx", []float64{-87.1875, -85.78125, 88.59375, 90.0}}, + {"3v4n", []float64{-15.8203125, -15.64453125, -98.4375, -98.0859375}}, + {"0z1theg", []float64{-49.7254943848, -49.7241210938, -143.938751221, -143.93737793}}, + {"zbz00jf21m", []float64{49.2503625154, 49.2503678799, 178.596893549, 178.596904278}}, + {"dfpq2eg2", []float64{12.3692321777, 12.3694038391, -46.0282516479, -46.0279083252}}, + {"z2j5bc1ph562", []float64{45.6658919156, 45.6658920832, 153.315756954, 153.31575729}}, + {"3p3g", []float64{-3.69140625, -3.515625, -132.5390625, -132.1875}}, + {"4rfgeu3", []float64{-45.7676696777, -45.7662963867, -74.7166442871, -74.7152709961}}, + {"nykq", []float64{-53.7890625, -53.61328125, 129.7265625, 130.078125}}, + {"h", []float64{-90.0, -45.0, 0.0, 45.0}}, + {"85", []float64{16.875, 22.5, -180.0, -168.75}}, + {"bdsdxr", []float64{59.5404052734, 59.5458984375, -150.853271484, -150.842285156}}, + {"wsyt3duqg2", []float64{27.657866478, 27.6578718424, 121.71251893, 121.712529659}}, + {"90", []float64{0.0, 5.625, -135.0, -123.75}}, + {"butw", []float64{71.3671875, 71.54296875, -138.515625, -138.1640625}}, + {"ddhpjv6b7tqh", []float64{12.5093796104, 12.5093797781, -61.6183796525, -61.6183793172}}, + {"18ueqgd", []float64{-85.1907348633, -85.1893615723, -105.872497559, -105.871124268}}, + {"v2g8jh1", []float64{49.2407226562, 49.2420959473, 61.3929748535, 61.3943481445}}, + {"84umeh3gmupk", []float64{16.45947285, 16.4594730176, -173.888941817, -173.888941482}}, + {"s4g900", []float64{15.64453125, 15.6500244141, 4.921875, 4.93286132812}}, + {"0b313fz2", []float64{-88.3589172363, -88.358745575, -144.756889343, -144.756546021}}, + {"4q", []float64{-56.25, -50.625, -78.75, -67.5}}, + {"d61", []float64{11.25, 12.65625, -77.34375, -75.9375}}, + {"w5q5298pq", []float64{18.8620233536, 18.8620662689, 98.4597301483, 98.4597730637}}, + {"ushgx399", []float64{68.1236457825, 68.1238174438, 29.5003509521, 29.5006942749}}, + {"73ngt", []float64{-38.759765625, -38.7158203125, -24.0380859375, -23.994140625}}, + {"2f4smcem", []float64{-32.9938316345, -32.9936599731, -142.477226257, -142.476882935}}, + {"0", []float64{-90.0, -45.0, -180.0, -135.0}}, + {"8", []float64{0.0, 45.0, -180.0, -135.0}}, + {"5u14weqgz", []float64{-67.0420503616, -67.0420074463, -9.54853534698, -9.54849243164}}, + {"xxhuu8y4xb", []float64{40.214509964, 40.2145153284, 164.386013746, 164.386024475}}, + {"272xeqmj", []float64{-25.3652000427, -25.3650283813, -167.897186279, -167.896842957}}, + {"2trrhunrd1", []float64{-14.215015769, -14.2150104046, -147.087278366, -147.087267637}}, + {"e4", []float64{11.25, 16.875, -45.0, -33.75}}, + {"p5duz8tp", []float64{-69.4735908508, -69.4734191895, 139.203643799, 139.203987122}}, + {"5qprz78e", []float64{-54.8679542542, -54.8677825928, -23.2353973389, -23.2350540161}}, + {"ch5yq40qtu3", []float64{68.6107577384, 68.6107590795, -129.462299198, -129.462297857}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"7qmv9c5", []float64{-8.87145996094, -8.87008666992, -25.5830383301, -25.5816650391}}, + {"c", []float64{45.0, 90.0, -135.0, -90.0}}, + {"hp8s7", []float64{-47.0654296875, -47.021484375, 0.8349609375, 0.87890625}}, + {"9e4y04d17", []float64{17.9436349869, 17.9436779022, -108.629937172, -108.629894257}}, + {"39nh", []float64{-38.671875, -38.49609375, -104.0625, -103.7109375}}, + {"6", []float64{-45.0, 0.0, -90.0, -45.0}}, + {"pjpxe1pvyzhm", []float64{-60.5501220189, -60.5501218513, 145.689649321, 145.689649656}}, + {"drx", []float64{42.1875, 43.59375, -68.90625, -67.5}}, + {"zu1c5qg0s", []float64{67.7129459381, 67.7129888535, 171.3580513, 171.358094215}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"t8d", []float64{2.8125, 4.21875, 70.3125, 71.71875}}, + {"d47w70rwe23h", []float64{13.7573739141, 13.7573740818, -84.9358485639, -84.9358482286}}, + {"3t617", []float64{-15.2490234375, -15.205078125, -109.555664062, -109.51171875}}, + {"qnkq1pz", []float64{-8.74649047852, -8.7451171875, 96.0301208496, 96.0314941406}}, + {"fu", []float64{67.5, 73.125, -56.25, -45.0}}, + {"7vs", []float64{-14.0625, -12.65625, -5.625, -4.21875}}, + {"bztqz0h", []float64{88.3740234375, 88.3753967285, -138.554077148, -138.552703857}}, + {"b8j", []float64{45.0, 46.40625, -150.46875, -149.0625}}, + {"cetkxmq73", []float64{65.5079126358, 65.5079555511, -104.789958, -104.789915085}}, + {"p91", []float64{-84.375, -82.96875, 158.90625, 160.3125}}, + {"z4g7bn4w38", []float64{61.1619615555, 61.1619669199, 139.573810101, 139.573820829}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"g2eej64jbwj", []float64{48.3518493176, 48.3518506587, -28.5946373641, -28.594636023}}, + {"fwshzb30mj", []float64{82.398903966, 82.3989093304, -61.5328359604, -61.5328252316}}, + {"fv2mqt", []float64{75.4815673828, 75.4870605469, -55.6127929688, -55.6018066406}}, + {"bzr6m3zdun5", []float64{86.1868751049, 86.186876446, -135.813499242, -135.813497901}}, + {"et5rq77j8b", []float64{29.4182109833, 29.4182163477, -17.6508772373, -17.6508665085}}, + {"1c", []float64{-84.375, -78.75, -101.25, -90.0}}, + {"y1hyumh10jq", []float64{51.8391890824, 51.8391904235, 96.8719562888, 96.8719576299}}, + {"qd42djnqxq", []float64{-33.6334955692, -33.6334902048, 115.76084733, 115.760858059}}, + {"hsd9s", []float64{-64.423828125, -64.3798828125, 26.19140625, 26.2353515625}}, + {"8289gq947", []float64{3.156208992, 3.15625190735, -167.902550697, -167.902507782}}, + {"em37sw72zq4", []float64{30.1809775829, 30.180978924, -31.7896565795, -31.7896552384}}, + {"zms25", []float64{75.9375, 75.9814453125, 152.358398438, 152.40234375}}, + {"h25d54", []float64{-89.6374511719, -89.6319580078, 16.3037109375, 16.3146972656}}, + {"6qc7y2t4bb", []float64{-6.36885166168, -6.36884629726, -76.7106306553, -76.7106199265}}, + {"06vt5z8j", []float64{-73.6102867126, -73.6101150513, -160.850830078, -160.850486755}}, + {"37q3", []float64{-26.54296875, -26.3671875, -114.9609375, -114.609375}}, + {"sey9wu", []float64{21.3793945312, 21.3848876953, 31.9372558594, 31.9482421875}}, + {"qk0jrj", []float64{-21.5496826172, -21.5441894531, 101.557617188, 101.568603516}}, + {"8x6jjpm0", []float64{41.6999816895, 41.7001533508, -154.460906982, -154.46056366}}, + {"5j1etu", []float64{-61.2377929688, -61.2322998047, -42.6379394531, -42.626953125}}, + {"r6b", []float64{-29.53125, -28.125, 146.25, 147.65625}}, + {"ddu3vyj07", []float64{15.8093690872, 15.8094120026, -61.263756752, -61.2637138367}}, + {"m9fm5q2d91", []float64{-34.2425769567, -34.2425715923, 70.8076143265, 70.8076250553}}, + {"0pxdx", []float64{-47.373046875, -47.3291015625, -169.145507812, -169.1015625}}, + {"w", []float64{0.0, 45.0, 90.0, 135.0}}, + {"q1e", []float64{-36.5625, -35.15625, 94.21875, 95.625}}, + {"h3vxhm8tu", []float64{-78.8945817947, -78.8945388794, 19.172000885, 19.1720438004}}, + {"bcxsz", []float64{54.2724609375, 54.31640625, -135.395507812, -135.3515625}}, + {"crjh", []float64{85.078125, 85.25390625, -116.71875, -116.3671875}}, + {"bdqejqqgwj", []float64{58.2185536623, 58.2185590267, -148.119134903, -148.119124174}}, + {"x7zhc480u7", []float64{21.9425886869, 21.9425940514, 156.137877703, 156.137888432}}, + {"xhr7c9nd6js9", []float64{24.5713387616, 24.5713389292, 145.270248726, 145.270249061}}, + {"f25r3r", []float64{46.3128662109, 46.318359375, -74.1247558594, -74.1137695312}}, + {"b4v1e8zek", []float64{60.7370996475, 60.7371425629, -172.804470062, -172.804427147}}, + {"95cwh1k", []float64{22.1553039551, 22.1566772461, -132.709350586, -132.707977295}}, + {"kh1r", []float64{-21.26953125, -21.09375, 1.7578125, 2.109375}}, + {"7p", []float64{-5.625, 0.0, -45.0, -33.75}}, + {"mgsvj", []float64{-24.43359375, -24.3896484375, 85.6494140625, 85.693359375}}, + {"k70", []float64{-28.125, -26.71875, 11.25, 12.65625}}, + {"pxjr5g", []float64{-49.3780517578, -49.3725585938, 165.047607422, 165.05859375}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"81pv", []float64{6.50390625, 6.6796875, -169.1015625, -168.75}}, + {"jjg", []float64{-57.65625, -56.25, 49.21875, 50.625}}, + {"732kjtvw", []float64{-37.2330093384, -37.232837677, -33.1491851807, -33.1488418579}}, + {"kuc2", []float64{-18.28125, -18.10546875, 35.5078125, 35.859375}}, + {"wn91fmw18yp", []float64{36.9006192684, 36.9006206095, 91.5134082735, 91.5134096146}}, + {"5wdnzyz5r", []float64{-52.2133398056, -52.2132968903, -19.3370103836, -19.3369674683}}, + {"m682wkeu80", []float64{-30.8241176605, -30.8241122961, 56.8813705444, 56.8813812733}}, + {"r18jv9k", []float64{-35.5448913574, -35.5435180664, 135.247192383, 135.248565674}}, + {"zr079yhvttr", []float64{85.0241656601, 85.0241670012, 146.685235351, 146.685236692}}, + {"r4umz4vhvm", []float64{-28.5045593977, -28.5045540333, 141.291271448, 141.291282177}}, + {"58gdwpzc3zs", []float64{-85.2989700437, -85.2989687026, -17.3037296534, -17.3037283123}}, + {"64frgqpt0yj", []float64{-28.1350958347, -28.1350944936, -86.6827766597, -86.6827753186}}, + {"8n18eckkw5", []float64{33.8455456495, 33.8455510139, -177.719736099, -177.71972537}}, + {"mz326c81b1", []float64{-4.16625916958, -4.16625380516, 80.6286621094, 80.6286728382}}, + {"hx", []float64{-50.625, -45.0, 22.5, 33.75}}, + {"ush2juq", []float64{67.5233459473, 67.5247192383, 28.737487793, 28.738861084}}, + {"bp6h7m8fe", []float64{86.5589618683, 86.5590047836, -177.04351902, -177.043476105}}, + {"111", []float64{-84.375, -82.96875, -133.59375, -132.1875}}, + {"m9hwzz", []float64{-38.1500244141, -38.14453125, 74.1687011719, 74.1796875}}, + {"100u6e92zuk", []float64{-89.2335520685, -89.2335507274, -133.833394647, -133.833393306}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"9wbmdmhu", []float64{38.9636993408, 38.9638710022, -112.043037415, -112.042694092}}, + {"9p1jg8k", []float64{40.3871154785, 40.3884887695, -133.434448242, -133.433074951}}, + {"vqf6kw", []float64{83.3972167969, 83.4027099609, 59.6118164062, 59.6228027344}}, + {"gw", []float64{78.75, 84.375, -22.5, -11.25}}, + {"h49v9", []float64{-74.970703125, -74.9267578125, 2.5048828125, 2.548828125}}, + {"23cmz", []float64{-34.1455078125, -34.1015625, -166.684570312, -166.640625}}, + {"71", []float64{-39.375, -33.75, -45.0, -33.75}}, + {"5x2kvmbu", []float64{-48.3515167236, -48.3513450623, -21.9166946411, -21.9163513184}}, + {"1nywfjs8e", []float64{-50.8144283295, -50.8143854141, -125.765175819, -125.765132904}}, + {"7u4vm3b9qy", []float64{-21.5672886372, -21.5672832727, -7.15112328529, -7.15111255646}}, + {"rx4n750gn", []float64{-4.50937271118, -4.50932979584, 160.445623398, 160.445666313}}, + {"9", []float64{0.0, 45.0, -135.0, -90.0}}, + {"nxfyqng3", []float64{-45.2703666687, -45.2701950073, 116.635322571, 116.635665894}}, + {"tgnt", []float64{17.75390625, 17.9296875, 87.890625, 88.2421875}}, + {"qe2k5jtbm2c", []float64{-25.985365659, -25.9853643179, 112.991521508, 112.991522849}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"1jhq", []float64{-60.8203125, -60.64453125, -129.0234375, -128.671875}}, + {"p874n4ubjm", []float64{-88.2270544767, -88.2270491123, 161.989170313, 161.989181042}}, + {"91h2qc", []float64{5.67443847656, 5.67993164062, -128.726806641, -128.715820312}}, + {"mzp8s0p", []float64{-5.537109375, -5.53573608398, 89.4822692871, 89.4836425781}}, + {"ptp0rem", []float64{-61.8132019043, -61.8118286133, 167.680206299, 167.68157959}}, + {"14", []float64{-78.75, -73.125, -135.0, -123.75}}, + {"s4dq", []float64{15.1171875, 15.29296875, 3.1640625, 3.515625}}, + {"uvs7", []float64{76.46484375, 76.640625, 39.7265625, 40.078125}}, + {"wh9xq3mqh", []float64{26.5948104858, 26.5948534012, 92.3914146423, 92.3914575577}}, + {"kz", []float64{-5.625, 0.0, 33.75, 45.0}}, + {"s8t4hkb3", []float64{3.19032669067, 3.19049835205, 29.7183609009, 29.7187042236}}, + {"3ry9w", []float64{-1.142578125, -1.0986328125, -114.345703125, -114.301757812}}, + {"mf1wt5", []float64{-32.5909423828, -32.5854492188, 81.0791015625, 81.0900878906}}, + {"e", []float64{0.0, 45.0, -45.0, 0.0}}, + {"mh", []float64{-22.5, -16.875, 45.0, 56.25}}, + {"75y3665k", []float64{-23.6748504639, -23.6746788025, -36.1075973511, -36.1072540283}}, + {"sts", []float64{30.9375, 32.34375, 28.125, 29.53125}}, + {"6fdmxb", []float64{-29.970703125, -29.9652099609, -52.7453613281, -52.734375}}, + {"xcvf9qbx13jx", []float64{10.3214901499, 10.3214903176, 176.891616806, 176.891617142}}, + {"n1r6pkxu86t", []float64{-82.5916823745, -82.5916810334, 100.524576455, 100.524577796}}, + {"1m2p", []float64{-59.23828125, -59.0625, -123.75, -123.3984375}}, + {"fz", []float64{84.375, 90.0, -56.25, -45.0}}, + {"hgw", []float64{-70.3125, -68.90625, 42.1875, 43.59375}}, + {"ssktp8e5hsub", []float64{24.7884432971, 24.7884434648, 29.1620342061, 29.1620345414}}, + {"8wbw4", []float64{39.0234375, 39.0673828125, -156.708984375, -156.665039062}}, + {"wbcdsqtpnkh9", []float64{4.69513194636, 4.69513211399, 126.053283289, 126.053283624}}, + {"f0md", []float64{46.7578125, 46.93359375, -82.265625, -81.9140625}}, + {"hngnmbt2pe4", []float64{-50.9298545122, -50.9298531711, 4.478969872, 4.4789712131}}, + {"gbkn8cgjewxc", []float64{47.559420336, 47.5594205037, -5.58776054531, -5.58776021004}}, + {"u", []float64{45.0, 90.0, 0.0, 45.0}}, + {"b5", []float64{61.875, 67.5, -180.0, -168.75}}, + {"w1r042nrqyk", []float64{7.0325280726, 7.0325294137, 99.951505065, 99.9515064061}}, + {"tv6r1uuh1h", []float64{30.7885193825, 30.7885247469, 81.9965028763, 81.9965136051}}, + {"r1hw8pqpr3", []float64{-38.1913465261, -38.1913411617, 141.336675882, 141.336686611}}, + {"j2bcyrxdgj", []float64{-85.4319351912, -85.4319298267, 57.5897741318, 57.5897848606}}, + {"m", []float64{-45.0, 0.0, 45.0, 90.0}}, + {"qtxvgfvmrbf", []float64{-13.0357463658, -13.0357450247, 123.570777476, 123.570778817}}, + {"jp", []float64{-50.625, -45.0, 45.0, 56.25}}, + {"f76", []float64{63.28125, 64.6875, -75.9375, -74.53125}}, + {"vz9", []float64{87.1875, 88.59375, 80.15625, 81.5625}}, + {"wm", []float64{28.125, 33.75, 101.25, 112.5}}, + {"c0wn5", []float64{48.8671875, 48.9111328125, -126.430664062, -126.38671875}}, + {"7pn7whg", []float64{-4.9836730957, -4.98229980469, -35.943145752, -35.9417724609}}, + {"s", []float64{0.0, 45.0, 0.0, 45.0}}, + {"txwr3", []float64{43.4619140625, 43.505859375, 76.3330078125, 76.376953125}}, + {"zc0", []float64{50.625, 52.03125, 168.75, 170.15625}}, + {"sq7pru6gr", []float64{36.4545679092, 36.4546108246, 15.8134031296, 15.8134460449}}, + {"nu", []float64{-67.5, -61.875, 123.75, 135.0}}, + {"7dkt6vr", []float64{-31.3920593262, -31.3906860352, -16.0414123535, -16.0400390625}}, + {"xm2uwefdyf1", []float64{30.3433477879, 30.343349129, 147.594056278, 147.59405762}}, + {"mgmnc0", []float64{-25.5322265625, -25.5267333984, 85.8251953125, 85.8361816406}}, + {"jj1shq", []float64{-61.1389160156, -61.1334228516, 47.2961425781, 47.3071289062}}, + {"3", []float64{-45.0, 0.0, -135.0, -90.0}}, + {"0p4y3p8tgr", []float64{-49.4841438532, -49.4841384888, -176.088041067, -176.088030338}}, + {"gu", []float64{67.5, 73.125, -11.25, 0.0}}, + {"e94", []float64{5.625, 7.03125, -19.6875, -18.28125}}, + {"u7khr", []float64{64.0283203125, 64.072265625, 17.1826171875, 17.2265625}}, + {"k1k", []float64{-37.96875, -36.5625, 5.625, 7.03125}}, + {"wks48m7f", []float64{25.7811355591, 25.7813072205, 106.891136169, 106.891479492}}, + {"z91w3", []float64{51.7236328125, 51.767578125, 159.653320312, 159.697265625}}, + {"c2d6xmbp1", []float64{48.284740448, 48.2847833633, -120.267291069, -120.267248154}}, + {"s9yur", []float64{10.5908203125, 10.634765625, 32.2998046875, 32.34375}}, + {"7u09b46", []float64{-22.1800231934, -22.1786499023, -10.544128418, -10.542755127}}, + {"8sndxb", []float64{22.939453125, 22.9449462891, -148.018798828, -148.0078125}}, + {"j2g761bhsex", []float64{-85.1995566487, -85.1995553076, 60.9084056318, 60.9084069729}}, + {"5wg", []float64{-52.03125, -50.625, -18.28125, -16.875}}, + {"fzn", []float64{84.375, 85.78125, -47.8125, -46.40625}}, + {"ugdpz8p4mu", []float64{66.0502123833, 66.0502177477, 36.9019496441, 36.9019603729}}, + {"nx", []float64{-50.625, -45.0, 112.5, 123.75}}, + {"d", []float64{0.0, 45.0, -90.0, -45.0}}, + {"crr9e8mz59se", []float64{86.0475053452, 86.0475055128, -113.041263744, -113.041263409}}, + {"dgmpsg", []float64{19.6160888672, 19.6215820312, -49.0100097656, -48.9990234375}}, + {"jcfk52m03", []float64{-79.4517087936, -79.4516658783, 82.063794136, 82.0638370514}}, + {"d8trpccuk", []float64{4.05331134796, 4.05335426331, -59.7740364075, -59.7739934921}}, + {"93g9665", []float64{10.0744628906, 10.0758361816, -118.725128174, -118.723754883}}, + {"sqt7cuf8d0f", []float64{37.2478620708, 37.2478634119, 18.7132385373, 18.7132398784}}, + {"f9", []float64{50.625, 56.25, -67.5, -56.25}}, + {"k90", []float64{-39.375, -37.96875, 22.5, 23.90625}}, + {"k8xdhcv", []float64{-41.8263244629, -41.8249511719, 33.2624816895, 33.2638549805}}, + {"4989w4r926t7", []float64{-81.2862400152, -81.2862398475, -66.5228856727, -66.5228853375}}, + {"c3", []float64{50.625, 56.25, -123.75, -112.5}}, + {"bd908pg0", []float64{59.1929626465, 59.1931343079, -156.089630127, -156.089286804}}, + {"bq", []float64{78.75, 84.375, -168.75, -157.5}}, + {"chcdt", []float64{72.158203125, 72.2021484375, -132.670898438, -132.626953125}}, + {"hff8vsrzhy39", []float64{-74.3748327903, -74.3748326227, 37.5181730837, 37.5181734189}}, + {"9gef7g6ezj", []float64{20.101531148, 20.1015365124, -95.8080339432, -95.8080232143}}, + {"yc0u2dp", []float64{51.3830566406, 51.3844299316, 124.836273193, 124.837646484}}, + {"w0b41f7", []float64{4.58267211914, 4.58404541016, 90.0810241699, 90.0823974609}}, + {"8cdmwjc", []float64{9.43588256836, 9.43725585938, -142.820892334, -142.819519043}}, + {"p4ngqtjm2", []float64{-78.150343895, -78.1503009796, 144.785041809, 144.785084724}}, + {"5", []float64{-90.0, -45.0, -45.0, 0.0}}, + {"3qtzqdknx", []float64{-7.14961051941, -7.14956760406, -115.372624397, -115.372581482}}, + {"gzjhuv9xmkg5", []float64{85.2414438687, 85.2414440364, -4.00772050023, -4.00772016495}}, + {"8g8t7eh4y0fh", []float64{20.6273078173, 20.627307985, -145.387313068, -145.387312733}}, + {"39w", []float64{-36.5625, -35.15625, -104.0625, -102.65625}}, + {"z34rj8", []float64{51.85546875, 51.8609619141, 149.655761719, 149.666748047}}, + {"c9p0zv2", []float64{50.7856750488, 50.7870483398, -102.315673828, -102.314300537}}, + {"vh871y", []float64{70.8728027344, 70.8782958984, 45.4284667969, 45.439453125}}, + {"7ggt8b4yfw", []float64{-22.9382622242, -22.9382568598, -6.29128217697, -6.29127144814}}, + {"3qz3d324z", []float64{-6.76023960114, -6.76019668579, -113.455510139, -113.455467224}}, + {"4sm2463w0w", []float64{-66.0803282261, -66.0803228617, -60.0162291527, -60.0162184238}}, + {"26ewbu0gw3", []float64{-29.728397727, -29.7283923626, -163.793867826, -163.793857098}}, + {"bre7js2w9z", []float64{87.7393430471, 87.7393484116, -163.937226534, -163.937215805}}, + {"sy5ug08bn", []float64{34.5877075195, 34.5877504349, 39.1565608978, 39.1566038132}}, + {"p4r", []float64{-77.34375, -75.9375, 144.84375, 146.25}}, + {"qb", []float64{-45.0, -39.375, 123.75, 135.0}}, + {"f4hj", []float64{57.12890625, 57.3046875, -84.375, -84.0234375}}, + {"5r0f5t7d5", []float64{-50.2442550659, -50.2442121506, -32.5365686417, -32.5365257263}}, + {"2j7n4r6r", []float64{-14.3730354309, -14.3728637695, -175.679283142, -175.678939819}}, + {"wu92egv2wsy", []float64{25.4211013019, 25.421102643, 125.680104196, 125.680105537}}, + {"vgtwey347bg", []float64{65.8648006618, 65.8648020029, 86.6507081687, 86.6507095098}}, + {"q2meny7pbd18", []float64{-43.0307328701, -43.0307327025, 109.285149202, 109.285149537}}, + {"rpe", []float64{-2.8125, -1.40625, 139.21875, 140.625}}, + {"m69", []float64{-30.9375, -29.53125, 57.65625, 59.0625}}, + {"w1zwd8", []float64{10.986328125, 10.9918212891, 100.656738281, 100.667724609}}, + {"fzpf", []float64{84.7265625, 84.90234375, -45.3515625, -45.0}}, + {"t3w", []float64{8.4375, 9.84375, 64.6875, 66.09375}}, + {"zb11", []float64{45.17578125, 45.3515625, 170.15625, 170.5078125}}, + {"r2prkmxpgtww", []float64{-43.6940126494, -43.6940124817, 156.641852036, 156.641852371}}, + {"zr34g1zcj", []float64{86.274433136, 86.2744760513, 147.79894352, 147.798986435}}, + {"19mgdk8", []float64{-82.3287963867, -82.3274230957, -104.315185547, -104.313812256}}, + {"mkp", []float64{-22.5, -21.09375, 66.09375, 67.5}}, + {"934qy86ssc", []float64{6.81367456913, 6.81367993355, -120.296655893, -120.296645164}}, + {"byydj4mrm8", []float64{83.3339166641, 83.3339220285, -136.882202625, -136.882191896}}, + {"j", []float64{-90.0, -45.0, 45.0, 90.0}}, + {"9cjzqv5n6", []float64{6.92795276642, 6.92799568176, -92.8632259369, -92.8631830215}}, + {"vkg1wg6s", []float64{72.0009613037, 72.0011329651, 60.7688140869, 60.7691574097}}, + {"ynp42e9x", []float64{79.1659355164, 79.1661071777, 99.8677825928, 99.8681259155}}, + {"uv9zwddtwn", []float64{77.2705686092, 77.2705739737, 36.5002727509, 36.5002834797}}, + {"t17zkzszpm", []float64{8.3480912447, 8.34809660912, 50.4890120029, 50.4890227318}}, + {"tuw779c04ukm", []float64{25.8934257366, 25.8934259042, 87.6943681017, 87.6943684369}}, + {"37rm598", []float64{-25.8316040039, -25.8302307129, -113.400878906, -113.399505615}}, + {"ymf18", []float64{77.607421875, 77.6513671875, 104.0625, 104.106445312}}, + {"gd", []float64{56.25, 61.875, -22.5, -11.25}}, + {"smz", []float64{32.34375, 33.75, 21.09375, 22.5}}, + {"p", []float64{-90.0, -45.0, 135.0, 180.0}}, + {"muzkh95w2u42", []float64{-17.5715374947, -17.571537327, 89.1479081288, 89.1479084641}}, + {"hh53c48eg", []float64{-67.1780061722, -67.1779632568, 4.61507320404, 4.61511611938}}, + {"739ewv", []float64{-35.9197998047, -35.9143066406, -31.3439941406, -31.3330078125}}, + {"cw883", []float64{81.6064453125, 81.650390625, -111.752929688, -111.708984375}}, + {"41xu1w37yf", []float64{-80.8243882656, -80.8243829012, -79.0336382389, -79.0336275101}}, + {"0y750v2k", []float64{-54.2868804932, -54.2867088318, -141.997947693, -141.99760437}}, + {"gqbgw", []float64{83.583984375, 83.6279296875, -32.431640625, -32.3876953125}}, + {"pej", []float64{-73.125, -71.71875, 164.53125, 165.9375}}, + {"r05t", []float64{-44.12109375, -43.9453125, 139.921875, 140.2734375}}, + {"qfuew7wk4f", []float64{-28.8960921764, -28.896086812, 130.361484289, 130.361495018}}, + {"nu357yhp72y", []float64{-65.4882533848, -65.4882520437, 125.326685607, 125.326686949}}, + {"8qt7rnggz", []float64{37.1715116501, 37.1715545654, -161.054120064, -161.054077148}}, + {"dhjq2nz", []float64{23.6357116699, 23.6370849609, -82.6075744629, -82.6062011719}}, + {"5s4", []float64{-67.5, -66.09375, -19.6875, -18.28125}}, + {"ge8nq842", []float64{65.7861328125, 65.7863044739, -22.211265564, -22.2109222412}}, + {"71", []float64{-39.375, -33.75, -45.0, -33.75}}, + {"sz59kteks87q", []float64{39.625713788, 39.6257139556, 38.8742895797, 38.874289915}}, + {"ur2bh", []float64{85.78125, 85.8251953125, 12.48046875, 12.5244140625}}, + {"w140u2f", []float64{5.76095581055, 5.76232910156, 93.0020141602, 93.0033874512}}, + {"fpd3zkt6whz4", []float64{87.5202913955, 87.5202915631, -86.5098573267, -86.5098569915}}, + {"zmej764h8kb8", []float64{76.8721358478, 76.8721360154, 150.614330247, 150.614330582}}, + {"k4p6z", []float64{-33.2666015625, -33.22265625, 10.5029296875, 10.546875}}, + {"f8", []float64{45.0, 50.625, -67.5, -56.25}}, + {"utsy6pv17m", []float64{77.0789462328, 77.0789515972, 29.2745840549, 29.2745947838}}, + {"6z5", []float64{-5.625, -4.21875, -52.03125, -50.625}}, + {"mjdc1", []float64{-13.88671875, -13.8427734375, 48.9111328125, 48.955078125}}, + {"gjks4c2", []float64{75.2412414551, 75.2426147461, -38.5510253906, -38.5496520996}}, + {"fvkvvrh42ju", []float64{75.5808614194, 75.5808627605, -49.3341010809, -49.3340997398}}, + {"yp63x30p7u7", []float64{86.0516823828, 86.0516837239, 93.4828309715, 93.4828323126}}, + {"6rw", []float64{-2.8125, -1.40625, -70.3125, -68.90625}}, + {"28vsqm8sb6", []float64{-40.0031411648, -40.0031358004, -149.490269423, -149.490258694}}, + {"be72g2zcek5", []float64{63.4174847603, 63.4174861014, -152.776078731, -152.77607739}}, + {"xry6fn699f", []float64{44.1117489338, 44.1117542982, 155.130461454, 155.130472183}}, + {"3sf7bw01y4", []float64{-17.5888001919, -17.5887948275, -109.313707352, -109.313696623}}, + {"k729yrqr77", []float64{-26.3700467348, -26.3700413704, 12.2365057468, 12.2365164757}}, + {"e", []float64{0.0, 45.0, -45.0, 0.0}}, + {"63x6dd", []float64{-36.1120605469, -36.1065673828, -68.4448242188, -68.4338378906}}, + {"z", []float64{45.0, 90.0, 135.0, 180.0}}, + {"mzyj", []float64{-0.52734375, -0.3515625, 87.1875, 87.5390625}}, + {"3j6r", []float64{-14.23828125, -14.0625, -131.8359375, -131.484375}}, + {"u3q", []float64{52.03125, 53.4375, 19.6875, 21.09375}}, + {"nueu7mbnbn4", []float64{-63.9076530933, -63.9076517522, 129.166262448, 129.166263789}}, + {"cyq2pkr", []float64{80.1795959473, 80.1809692383, -92.1327209473, -92.1313476562}}, + {"gptzzke9hvh", []float64{88.5747224092, 88.5747237504, -36.5904432535, -36.5904419124}}, + {"khd", []float64{-19.6875, -18.28125, 2.8125, 4.21875}}, + {"ghm92hg6p5", []float64{69.1524285078, 69.1524338722, -37.2608613968, -37.260850668}}, + {"n9e20w0", []float64{-81.5295410156, -81.5281677246, 117.092285156, 117.093658447}}, + {"826dzs0k", []float64{1.91230773926, 1.91247940063, -164.904441833, -164.904098511}}, + {"0d5f2", []float64{-78.3544921875, -78.310546875, -152.2265625, -152.182617188}}, + {"70zsyg3", []float64{-39.9284362793, -39.9270629883, -34.1551208496, -34.1537475586}}, + {"zykh8gwy", []float64{80.9675216675, 80.9676933289, 174.417228699, 174.417572021}}, + {"4spd3s", []float64{-67.0825195312, -67.0770263672, -56.8872070312, -56.8762207031}}, + {"r9p6f2t", []float64{-38.8888549805, -38.8874816895, 167.801055908, 167.802429199}}, + {"6q3merq7w", []float64{-8.83652687073, -8.83648395538, -76.8405246735, -76.8404817581}}, + {"qx1zr32", []float64{-4.34371948242, -4.34234619141, 115.279541016, 115.280914307}}, + {"3zfnk", []float64{-0.3076171875, -0.263671875, -98.26171875, -98.2177734375}}, + {"kd2e3", []float64{-31.7724609375, -31.728515625, 23.2470703125, 23.291015625}}, + {"stkhr6", []float64{30.2893066406, 30.2947998047, 28.4436035156, 28.4545898438}}, + {"nh4pzbm", []float64{-66.1363220215, -66.1349487305, 93.159942627, 93.161315918}}, + {"zt8tf", []float64{76.9482421875, 76.9921875, 158.291015625, 158.334960938}}, + {"gd37", []float64{58.18359375, 58.359375, -20.7421875, -20.390625}}, + {"gnx5b45dd", []float64{82.2330951691, 82.2331380844, -35.1513576508, -35.1513147354}}, + {"2qm7", []float64{-9.31640625, -9.140625, -161.3671875, -161.015625}}, + {"4g0zf0kq9cpp", []float64{-71.7601996846, -71.760199517, -55.1015008986, -55.1015005633}}, + {"977tgt", []float64{19.3194580078, 19.3249511719, -118.674316406, -118.663330078}}, + {"md0k", []float64{-33.046875, -32.87109375, 67.8515625, 68.203125}}, + {"v1q3nvfb3h", []float64{52.2386813164, 52.2386866808, 54.089512825, 54.0895235538}}, + {"z96pt6u96w", []float64{53.3649623394, 53.3649677038, 160.549499989, 160.549510717}}, + {"pu", []float64{-67.5, -61.875, 168.75, 180.0}}, + {"6uydh", []float64{-17.9296875, -17.8857421875, -46.93359375, -46.8896484375}}, + {"nx5mt4sk", []float64{-49.6437835693, -49.643611908, 117.295875549, 117.296218872}}, + {"nk8jt", []float64{-63.720703125, -63.6767578125, 101.469726562, 101.513671875}}, + {"kec1015b", []float64{-23.7249755859, -23.7248039246, 23.9113998413, 23.9117431641}}, + {"fk388b", []float64{68.994140625, 68.9996337891, -76.6076660156, -76.5966796875}}, + {"nsb", []float64{-63.28125, -61.875, 112.5, 113.90625}}, + {"ndbws", []float64{-73.388671875, -73.3447265625, 113.37890625, 113.422851562}}, + {"fs5", []float64{67.5, 68.90625, -63.28125, -61.875}}, + {"h6x0kbec6p15", []float64{-75.8905554749, -75.8905553073, 21.3077272475, 21.3077275828}}, + {"hy78", []float64{-54.84375, -54.66796875, 38.671875, 39.0234375}}, + {"vpun9j4ce9", []float64{89.7640568018, 89.7640621662, 50.6728720665, 50.6728827953}}, + {"6tyevvqrj665", []float64{-11.9670169987, -11.967016831, -58.0978783965, -58.0978780612}}, + {"d3ktekr48n", []float64{8.02185416222, 8.02185952663, -72.2694396973, -72.2694289684}}, + {"heyfm2up5", []float64{-68.5054206848, -68.5053777695, 32.2285223007, 32.2285652161}}, + {"mn3f7qjr22v", []float64{-9.41403463483, -9.41403329372, 47.6109869778, 47.6109883189}}, + {"1wngqx9b", []float64{-55.637512207, -55.6373405457, -102.719764709, -102.719421387}}, + {"xc9jv49jej", []float64{9.46294605732, 9.46295142174, 170.3774786, 170.377489328}}, + {"27", []float64{-28.125, -22.5, -168.75, -157.5}}, + {"6yhqnw", []float64{-10.1623535156, -10.1568603516, -49.9877929688, -49.9768066406}}, + {"rmhhu3qd", []float64{-16.0328292847, -16.0326576233, 152.07069397, 152.071037292}}, + {"y00b8mhby", []float64{45.1154851913, 45.1155281067, 91.0724544525, 91.0724973679}}, + {"yq5tr", []float64{79.6728515625, 79.716796875, 106.479492188, 106.5234375}}, + {"cuvxw", []float64{73.037109375, 73.0810546875, -93.251953125, -93.2080078125}}, + {"exvb4bcn", []float64{43.5988998413, 43.5990715027, -14.2918395996, -14.2914962769}}, + {"uhdpsyx75n0", []float64{71.667112112, 71.6671134531, 3.03132534027, 3.03132668138}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"qvwbzgw7q", []float64{-13.9108800888, -13.9108371735, 133.591604233, 133.591647148}}, + {"u0rqp0bres", []float64{47.466366291, 47.4663716555, 10.503423214, 10.5034339428}}, + {"43k2u0vbnrbd", []float64{-82.8327522799, -82.8327521123, -72.5894909352, -72.5894905999}}, + {"7r7kwz7xxr", []float64{-3.38658392429, -3.38657855988, -28.8779389858, -28.877928257}}, + {"fu2f", []float64{69.2578125, 69.43359375, -55.1953125, -54.84375}}, + {"9tkjsvzxw6", []float64{30.5309307575, 30.5309361219, -106.655691862, -106.655681133}}, + {"y9x0z", []float64{53.5693359375, 53.61328125, 122.651367188, 122.6953125}}, + {"y4rk4b25g", []float64{58.3613920212, 58.3614349365, 100.316290855, 100.316333771}}, + {"sxmdmu9xcn", []float64{41.202839613, 41.2028449774, 30.4891633987, 30.4891741276}}, + {"fb0", []float64{45.0, 46.40625, -56.25, -54.84375}}, + {"7ffkxt5", []float64{-28.7127685547, -28.7113952637, -7.7522277832, -7.75085449219}}, + {"n1x5jn4dr", []float64{-81.0018110275, -81.0017681122, 100.067210197, 100.067253113}}, + {"uxcdctgmj", []float64{89.1095924377, 89.1096353531, 24.6799707413, 24.6800136566}}, + {"h7", []float64{-73.125, -67.5, 11.25, 22.5}}, + {"b", []float64{45.0, 90.0, -180.0, -135.0}}, + {"us1n4udk", []float64{68.5800933838, 68.5802650452, 24.0301895142, 24.0305328369}}, + {"zjtptsj2vn", []float64{77.2779929638, 77.2779983282, 142.280373573, 142.280384302}}, + {"x6utsqz", []float64{16.4726257324, 16.4739990234, 152.774505615, 152.775878906}}, + {"cs9dn901rqn", []float64{70.6698024273, 70.6698037684, -110.104661286, -110.104659945}}, + {"sjbn", []float64{33.3984375, 33.57421875, 0.0, 0.3515625}}, + {"0fwxjyc3g9q", []float64{-74.6696452796, -74.6696439385, -136.854814589, -136.854813248}}, + {"fk", []float64{67.5, 73.125, -78.75, -67.5}}, + {"75hq9jh", []float64{-26.9549560547, -26.9535827637, -38.9739990234, -38.9726257324}}, + {"kr3hg1q1", []float64{-3.37675094604, -3.37657928467, 12.7963256836, 12.7966690063}}, + {"hfq4d2wu", []float64{-76.9008636475, -76.9006919861, 42.2956466675, 42.2959899902}}, + {"rg6ygh", []float64{-25.5102539062, -25.5047607422, 172.749023438, 172.760009766}}, + {"995pvrg", []float64{7.02987670898, 7.03125, -108.046417236, -108.045043945}}, + {"s5ys", []float64{21.796875, 21.97265625, 9.140625, 9.4921875}}, + {"289ucubzj6", []float64{-41.3252341747, -41.3252288103, -154.960902929, -154.9608922}}, + {"4", []float64{-90.0, -45.0, -90.0, -45.0}}, + {"7g0e3gp7cz", []float64{-27.5365501642, -27.5365447998, -10.4599392414, -10.4599285126}}, + {"9suuudg", []float64{27.5688171387, 27.5701904297, -105.618438721, -105.61706543}}, + {"8vdt3j8zb0", []float64{31.8918943405, 31.8918997049, -142.689399719, -142.68938899}}, + {"cf", []float64{56.25, 61.875, -101.25, -90.0}}, + {"jnp33f5pr9", []float64{-56.0180372, -56.0180318356, 55.276658535, 55.2766692638}}, + {"czgmgyb", []float64{89.6415710449, 89.6429443359, -96.5148925781, -96.5135192871}}, + {"c1kk", []float64{52.734375, 52.91015625, -129.0234375, -128.671875}}, + {"kfm4hfe8cp4s", []float64{-31.9782876223, -31.9782874547, 40.994843021, 40.9948433563}}, + {"9mnws4hc8h", []float64{29.2788434029, 29.2788487673, -114.427070618, -114.427059889}}, + {"t0j7chwg", []float64{0.684413909912, 0.684585571289, 52.4360275269, 52.4363708496}}, + {"y", []float64{45.0, 90.0, 90.0, 135.0}}, + {"suj", []float64{22.5, 23.90625, 40.78125, 42.1875}}, + } + + for _, test := range tests { + lat, lon := DecodeGeoHash(test.hash) + + if !compareLatitude(test.box, lat) { + t.Errorf("expected lat %f, got %f, hash %s", (test.box[0]+test.box[1])/2, lat, test.hash) + } + if !compareLogitude(test.box, lon) { + t.Errorf("expected lon %f, got %f, hash %s", (test.box[2]+test.box[3])/2, lon, test.hash) + } + } +} + +func compareLatitude(box []float64, v float64) bool { + avg := (box[0] + box[1]) / 2 + + return compareGeo(avg, v) == 0 +} + +func compareLogitude(box []float64, v float64) bool { + avg := (box[2] + box[3]) / 2 + + return compareGeo(avg, v) == 0 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..76246ab --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module github.com/blevesearch/bleve/v2 + +go 1.23 + +toolchain go1.23.9 + +require ( + github.com/RoaringBitmap/roaring/v2 v2.4.5 + github.com/bits-and-blooms/bitset v1.22.0 + github.com/blevesearch/bleve_index_api v1.2.8 + github.com/blevesearch/geo v0.2.3 + github.com/blevesearch/go-faiss v1.0.25 + github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 + github.com/blevesearch/go-porterstemmer v1.0.3 + github.com/blevesearch/goleveldb v1.0.1 + github.com/blevesearch/gtreap v0.1.1 + github.com/blevesearch/scorch_segment_api/v2 v2.3.10 + github.com/blevesearch/segment v0.9.1 + github.com/blevesearch/snowball v0.6.1 + github.com/blevesearch/snowballstem v0.9.0 + github.com/blevesearch/stempel v0.2.0 + github.com/blevesearch/upsidedown_store_api v1.0.2 + github.com/blevesearch/vellum v1.1.0 + github.com/blevesearch/zapx/v11 v11.4.2 + github.com/blevesearch/zapx/v12 v12.4.2 + github.com/blevesearch/zapx/v13 v13.4.2 + github.com/blevesearch/zapx/v14 v14.4.2 + github.com/blevesearch/zapx/v15 v15.4.2 + github.com/blevesearch/zapx/v16 v16.2.3 + github.com/couchbase/moss v0.2.0 + github.com/golang/protobuf v1.3.2 + github.com/spf13/cobra v1.8.1 + go.etcd.io/bbolt v1.4.0 + golang.org/x/text v0.8.0 +) + +require ( + github.com/blevesearch/mmap-go v1.0.4 // indirect + github.com/couchbase/ghistogram v0.1.0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect + github.com/mschoch/smat v0.2.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + golang.org/x/sys v0.29.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3cd819c --- /dev/null +++ b/go.sum @@ -0,0 +1,122 @@ +github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= +github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y= +github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= +github.com/blevesearch/geo v0.2.3 h1:K9/vbGI9ehlXdxjxDRJtoAMt7zGAsMIzc6n8zWcwnhg= +github.com/blevesearch/geo v0.2.3/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= +github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U= +github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA= +github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= +github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= +github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= +github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI= +github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= +github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= +github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= +github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= +github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= +github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= +github.com/blevesearch/scorch_segment_api/v2 v2.3.10 h1:Yqk0XD1mE0fDZAJXTjawJ8If/85JxnLd8v5vG/jWE/s= +github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8= +github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= +github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= +github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A= +github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= +github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= +github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= +github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc= +github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc= +github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= +github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= +github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= +github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= +github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= +github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= +github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= +github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= +github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= +github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.2.3 h1:7Y0r+a3diEvlazsncexq1qoFOcBd64xwMS7aDm4lo1s= +github.com/blevesearch/zapx/v16 v16.2.3/go.mod h1:wVJ+GtURAaRG9KQAMNYyklq0egV+XJlGcXNCE0OFjjA= +github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps= +github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= +github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o= +github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede h1:YrgBGwxMRK0Vq0WSCWFaZUnTsrA/PZE/xs1QZh+/edg= +github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/index.go b/index.go new file mode 100644 index 0000000..3d23898 --- /dev/null +++ b/index.go @@ -0,0 +1,388 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/index/upsidedown" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +// A Batch groups together multiple Index and Delete +// operations you would like performed at the same +// time. The Batch structure is NOT thread-safe. +// You should only perform operations on a batch +// from a single thread at a time. Once batch +// execution has started, you may not modify it. +type Batch struct { + index Index + internal *index.Batch + + lastDocSize uint64 + totalSize uint64 +} + +// Index adds the specified index operation to the +// batch. NOTE: the bleve Index is not updated +// until the batch is executed. +func (b *Batch) Index(id string, data interface{}) error { + if id == "" { + return ErrorEmptyID + } + if eventIndex, ok := b.index.(index.EventIndex); ok { + eventIndex.FireIndexEvent() + } + doc := document.NewDocument(id) + err := b.index.Mapping().MapDocument(doc, data) + if err != nil { + return err + } + b.internal.Update(doc) + + b.lastDocSize = uint64(doc.Size() + + len(id) + size.SizeOfString) // overhead from internal + b.totalSize += b.lastDocSize + + return nil +} + +func (b *Batch) IndexSynonym(id string, collection string, definition *SynonymDefinition) error { + if id == "" { + return ErrorEmptyID + } + if eventIndex, ok := b.index.(index.EventIndex); ok { + eventIndex.FireIndexEvent() + } + synMap, ok := b.index.Mapping().(mapping.SynonymMapping) + if !ok { + return ErrorSynonymSearchNotSupported + } + + if err := definition.Validate(); err != nil { + return err + } + + doc := document.NewSynonymDocument(id) + err := synMap.MapSynonymDocument(doc, collection, definition.Input, definition.Synonyms) + if err != nil { + return err + } + b.internal.Update(doc) + + b.lastDocSize = uint64(doc.Size() + + len(id) + size.SizeOfString) // overhead from internal + b.totalSize += b.lastDocSize + + return nil +} + +func (b *Batch) LastDocSize() uint64 { + return b.lastDocSize +} + +func (b *Batch) TotalDocsSize() uint64 { + return b.totalSize +} + +// IndexAdvanced adds the specified index operation to the +// batch which skips the mapping. NOTE: the bleve Index is not updated +// until the batch is executed. +func (b *Batch) IndexAdvanced(doc *document.Document) (err error) { + if doc.ID() == "" { + return ErrorEmptyID + } + b.internal.Update(doc) + return nil +} + +// Delete adds the specified delete operation to the +// batch. NOTE: the bleve Index is not updated until +// the batch is executed. +func (b *Batch) Delete(id string) { + if id != "" { + b.internal.Delete(id) + } +} + +// SetInternal adds the specified set internal +// operation to the batch. NOTE: the bleve Index is +// not updated until the batch is executed. +func (b *Batch) SetInternal(key, val []byte) { + b.internal.SetInternal(key, val) +} + +// DeleteInternal adds the specified delete internal +// operation to the batch. NOTE: the bleve Index is +// not updated until the batch is executed. +func (b *Batch) DeleteInternal(key []byte) { + b.internal.DeleteInternal(key) +} + +// Size returns the total number of operations inside the batch +// including normal index operations and internal operations. +func (b *Batch) Size() int { + return len(b.internal.IndexOps) + len(b.internal.InternalOps) +} + +// String prints a user friendly string representation of what +// is inside this batch. +func (b *Batch) String() string { + return b.internal.String() +} + +// Reset returns a Batch to the empty state so that it can +// be re-used in the future. +func (b *Batch) Reset() { + b.internal.Reset() + b.lastDocSize = 0 + b.totalSize = 0 +} + +func (b *Batch) Merge(o *Batch) { + if o != nil && o.internal != nil { + b.internal.Merge(o.internal) + if o.LastDocSize() > 0 { + b.lastDocSize = o.LastDocSize() + } + b.totalSize = uint64(b.internal.TotalDocSize()) + } +} + +func (b *Batch) SetPersistedCallback(f index.BatchCallback) { + b.internal.SetPersistedCallback(f) +} + +func (b *Batch) PersistedCallback() index.BatchCallback { + return b.internal.PersistedCallback() +} + +// An Index implements all the indexing and searching +// capabilities of bleve. An Index can be created +// using the New() and Open() methods. +// +// Index() takes an input value, deduces a DocumentMapping for its type, +// assigns string paths to its fields or values then applies field mappings on +// them. +// +// The DocumentMapping used to index a value is deduced by the following rules: +// 1. If value implements mapping.bleveClassifier interface, resolve the mapping +// from BleveType(). +// 2. If value implements mapping.Classifier interface, resolve the mapping +// from Type(). +// 3. If value has a string field or value at IndexMapping.TypeField. +// +// (defaulting to "_type"), use it to resolve the mapping. Fields addressing +// is described below. +// 4) If IndexMapping.DefaultType is registered, return it. +// 5) Return IndexMapping.DefaultMapping. +// +// Each field or nested field of the value is identified by a string path, then +// mapped to one or several FieldMappings which extract the result for analysis. +// +// Struct values fields are identified by their "json:" tag, or by their name. +// Nested fields are identified by prefixing with their parent identifier, +// separated by a dot. +// +// Map values entries are identified by their string key. Entries not indexed +// by strings are ignored. Entry values are identified recursively like struct +// fields. +// +// Slice and array values are identified by their field name. Their elements +// are processed sequentially with the same FieldMapping. +// +// String, float64 and time.Time values are identified by their field name. +// Other types are ignored. +// +// Each value identifier is decomposed in its parts and recursively address +// SubDocumentMappings in the tree starting at the root DocumentMapping. If a +// mapping is found, all its FieldMappings are applied to the value. If no +// mapping is found and the root DocumentMapping is dynamic, default mappings +// are used based on value type and IndexMapping default configurations. +// +// Finally, mapped values are analyzed, indexed or stored. See +// FieldMapping.Analyzer to know how an analyzer is resolved for a given field. +// +// Examples: +// +// type Date struct { +// Day string `json:"day"` +// Month string +// Year string +// } +// +// type Person struct { +// FirstName string `json:"first_name"` +// LastName string +// BirthDate Date `json:"birth_date"` +// } +// +// A Person value FirstName is mapped by the SubDocumentMapping at +// "first_name". Its LastName is mapped by the one at "LastName". The day of +// BirthDate is mapped to the SubDocumentMapping "day" of the root +// SubDocumentMapping "birth_date". It will appear as the "birth_date.day" +// field in the index. The month is mapped to "birth_date.Month". +type Index interface { + // Index analyzes, indexes or stores mapped data fields. Supplied + // identifier is bound to analyzed data and will be retrieved by search + // requests. See Index interface documentation for details about mapping + // rules. + Index(id string, data interface{}) error + Delete(id string) error + + NewBatch() *Batch + Batch(b *Batch) error + + // Document returns specified document or nil if the document is not + // indexed or stored. + Document(id string) (index.Document, error) + // DocCount returns the number of documents in the index. + DocCount() (uint64, error) + + Search(req *SearchRequest) (*SearchResult, error) + SearchInContext(ctx context.Context, req *SearchRequest) (*SearchResult, error) + + Fields() ([]string, error) + + FieldDict(field string) (index.FieldDict, error) + FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) + FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) + + Close() error + + Mapping() mapping.IndexMapping + + Stats() *IndexStat + StatsMap() map[string]interface{} + + GetInternal(key []byte) ([]byte, error) + SetInternal(key, val []byte) error + DeleteInternal(key []byte) error + + // Name returns the name of the index (by default this is the path) + Name() string + // SetName lets you assign your own logical name to this index + SetName(string) + + // Advanced returns the internal index implementation + Advanced() (index.Index, error) +} + +// New index at the specified path, must not exist. +// The provided mapping will be used for all +// Index/Search operations. +func New(path string, mapping mapping.IndexMapping) (Index, error) { + return newIndexUsing(path, mapping, Config.DefaultIndexType, Config.DefaultKVStore, nil) +} + +// NewMemOnly creates a memory-only index. +// The contents of the index is NOT persisted, +// and will be lost once closed. +// The provided mapping will be used for all +// Index/Search operations. +func NewMemOnly(mapping mapping.IndexMapping) (Index, error) { + return newIndexUsing("", mapping, upsidedown.Name, Config.DefaultMemKVStore, nil) +} + +// NewUsing creates index at the specified path, +// which must not already exist. +// The provided mapping will be used for all +// Index/Search operations. +// The specified index type will be used. +// The specified kvstore implementation will be used +// and the provided kvconfig will be passed to its +// constructor. Note that currently the values of kvconfig must +// be able to be marshaled and unmarshaled using the encoding/json library (used +// when reading/writing the index metadata file). +func NewUsing(path string, mapping mapping.IndexMapping, indexType string, kvstore string, kvconfig map[string]interface{}) (Index, error) { + return newIndexUsing(path, mapping, indexType, kvstore, kvconfig) +} + +// Open index at the specified path, must exist. +// The mapping used when it was created will be used for all Index/Search operations. +func Open(path string) (Index, error) { + return openIndexUsing(path, nil) +} + +// OpenUsing opens index at the specified path, must exist. +// The mapping used when it was created will be used for all Index/Search operations. +// The provided runtimeConfig can override settings +// persisted when the kvstore was created. +func OpenUsing(path string, runtimeConfig map[string]interface{}) (Index, error) { + return openIndexUsing(path, runtimeConfig) +} + +// Builder is a limited interface, used to build indexes in an offline mode. +// Items cannot be updated or deleted, and the caller MUST ensure a document is +// indexed only once. +type Builder interface { + Index(id string, data interface{}) error + Close() error +} + +// NewBuilder creates a builder, which will build an index at the specified path, +// using the specified mapping and options. +func NewBuilder(path string, mapping mapping.IndexMapping, config map[string]interface{}) (Builder, error) { + return newBuilder(path, mapping, config) +} + +// IndexCopyable is an index which supports an online copy operation +// of the index. +type IndexCopyable interface { + // CopyTo creates a fully functional copy of the index at the + // specified destination directory implementation. + CopyTo(d index.Directory) error +} + +// FileSystemDirectory is the default implementation for the +// index.Directory interface. +type FileSystemDirectory string + +// SynonymDefinition represents a synonym mapping in Bleve. +// Each instance associates one or more input terms with a list of synonyms, +// defining how terms are treated as equivalent in searches. +type SynonymDefinition struct { + // Input is an optional list of terms for unidirectional synonym mapping. + // When terms are specified in Input, they will map to the terms in Synonyms, + // making the relationship unidirectional (each Input maps to all Synonyms). + // If Input is omitted, the relationship is bidirectional among all Synonyms. + Input []string `json:"input,omitempty"` + + // Synonyms is a list of terms that are considered equivalent. + // If Input is specified, each term in Input will map to each term in Synonyms. + // If Input is not specified, the Synonyms list will be treated bidirectionally, + // meaning each term in Synonyms is treated as synonymous with all others. + Synonyms []string `json:"synonyms"` +} + +func (sd *SynonymDefinition) Validate() error { + if len(sd.Synonyms) == 0 { + return fmt.Errorf("synonym definition must have at least one synonym") + } + return nil +} + +// SynonymIndex supports indexing synonym definitions alongside regular documents. +// Synonyms, grouped by collection name, define term relationships for query expansion in searches. +type SynonymIndex interface { + Index + // IndexSynonym indexes a synonym definition, with the specified id and belonging to the specified collection. + IndexSynonym(id string, collection string, definition *SynonymDefinition) error +} diff --git a/index/scorch/README.md b/index/scorch/README.md new file mode 100644 index 0000000..fe2abde --- /dev/null +++ b/index/scorch/README.md @@ -0,0 +1,367 @@ +# scorch + +## Definitions + +Batch +- A collection of Documents to mutate in the index. + +Document +- Has a unique identifier (arbitrary bytes). +- Is comprised of a list of fields. + +Field +- Has a name (string). +- Has a type (text, number, date, geopoint). +- Has a value (depending on type). +- Can be indexed, stored, or both. +- If indexed, can be analyzed. +-m If indexed, can optionally store term vectors. + +## Scope + +Scorch *MUST* implement the bleve.index API without requiring any changes to this API. + +Scorch *MAY* introduce new interfaces, which can be discovered to allow use of new capabilities not in the current API. + +## Implementation + +The scorch implementation starts with the concept of a segmented index. + +A segment is simply a slice, subset, or portion of the entire index. A segmented index is one which is composed of one or more segments. Although segments are created in a particular order, knowing this ordering is not required to achieve correct semantics when querying. Because there is no ordering, this means that when searching an index, you can (and should) search all the segments concurrently. + +### Internal Wrapper + +In order to accommodate the existing APIs while also improving the implementation, the scorch implementation includes some wrapper functionality that must be described. + +#### \_id field + +In scorch, field 0 is prearranged to be named \_id. All documents have a value for this field, which is the documents external identifier. In this version the field *MUST* be both indexed AND stored. The scorch wrapper adds this field, as it will not be present in the Document from the calling bleve code. + +NOTE: If a document already contains a field \_id, it will be replaced. If this is problematic, the caller must ensure such a scenario does not happen. + +### Proposed Structures + +``` +type Segment interface { + + Dictionary(field string) TermDictionary + +} + +type TermDictionary interface { + + PostingsList(term string, excluding PostingsList) PostingsList + +} + +type PostingsList interface { + + Next() Posting + + And(other PostingsList) PostingsList + Or(other PostingsList) PostingsList + +} + +type Posting interface { + Number() uint64 + + Frequency() uint64 + Norm() float64 + + Locations() Locations +} + +type Locations interface { + Start() uint64 + End() uint64 + Pos() uint64 + ArrayPositions() ... +} + +type DeletedDocs { + +} + +type SegmentSnapshot struct { + segment Segment + deleted PostingsList +} + +type IndexSnapshot struct { + segment []SegmentSnapshot +} +``` +**What about errors?** +**What about memory mgmnt or context?** +**Postings List separate iterator to separate stateful from stateless** +### Mutating the Index + +The bleve.index API has methods for directly making individual mutations (Update/Delete/SetInternal/DeleteInternal), however for this first implementation, we assume that all of these calls can simply be turned into a Batch of size 1. This may be highly inefficient, but it will be correct. This decision is made based on the fact that Couchbase FTS always uses Batches. + +NOTE: As a side-effect of this decision, it should be clear that performance tuning may depend on the batch size, which may in-turn require changes in FTS. + +From this point forward, only Batch mutations will be discussed. + +Sequence of Operations: + +1. For each document in the batch, search through all existing segments. The goal is to build up a per-segment bitset which tells us which documents in that segment are obsoleted by the addition of the new segment we're currently building. NOTE: we're not ready for this change to take effect yet, so rather than this operation mutating anything, they simply return bitsets, which we can apply later. Logically, this is something like: + + ``` + foreach segment { + dict := segment.Dictionary("\_id") + postings := empty postings list + foreach docID { + postings = postings.Or(dict.PostingsList(docID, nil)) + } + } + ``` + + NOTE: it is illustrated above as nested for loops, but some or all of these could be concurrently. The end result is that for each segment, we have (possibly empty) bitset. + +2. Also concurrent with 1, the documents in the batch are analyzed. This analysis proceeds using the existing analyzer pool. + +3. (after 2 completes) Analyzed documents are fed into a function which builds a new Segment representing this information. + +4. We now have everything we need to update the state of the system to include this new snapshot. + + - Acquire a lock + - Create a new IndexSnapshot + - For each SegmentSnapshot in the IndexSnapshot, take the deleted PostingsList and OR it with the new postings list for this Segment. Construct a new SegmentSnapshot for the segment using this new deleted PostingsList. Append this SegmentSnapshot to the IndexSnapshot. + - Create a new SegmentSnapshot wrapping our new segment with nil deleted docs. + - Append the new SegmentSnapshot to the IndexSnapshot + - Release the lock + +An ASCII art example: + ``` + 0 - Empty Index + + No segments + + IndexSnapshot + segments [] + deleted [] + + + 1 - Index Batch [ A B C ] + + segment 0 + numbers [ 1 2 3 ] + \_id [ A B C ] + + IndexSnapshot + segments [ 0 ] + deleted [ nil ] + + + 2 - Index Batch [ B' ] + + segment 0 1 + numbers [ 1 2 3 ] [ 1 ] + \_id [ A B C ] [ B ] + + Compute bitset segment-0-deleted-by-1: + [ 0 1 0 ] + + OR it with previous (nil) (call it 0-1) + [ 0 1 0 ] + + IndexSnapshot + segments [ 0 1 ] + deleted [ 0-1 nil ] + + 3 - Index Batch [ C' ] + + segment 0 1 2 + numbers [ 1 2 3 ] [ 1 ] [ 1 ] + \_id [ A B C ] [ B ] [ C ] + + Compute bitset segment-0-deleted-by-2: + [ 0 0 1 ] + + OR it with previous ([ 0 1 0 ]) (call it 0-12) + [ 0 1 1 ] + + Compute bitset segment-1-deleted-by-2: + [ 0 ] + + OR it with previous (nil) + still just nil + + + IndexSnapshot + segments [ 0 1 2 ] + deleted [ 0-12 nil nil ] + ``` + +**is there opportunity to stop early when doc is found in one segment** +**also, more efficient way to find bits for long lists of ids?** + +### Searching + +In the bleve.index API all searching starts by getting an IndexReader, which represents a snapshot of the index at a point in time. + +As described in the section above, our index implementation maintains a pointer to the current IndexSnapshot. When a caller gets an IndexReader, they get a copy of this pointer, and can use it as long as they like. The IndexSnapshot contains SegmentSnapshots, which only contain pointers to immutable segments. The deleted posting lists associated with a segment change over time, but the particular deleted posting list in YOUR snapshot is immutable. This gives a stable view of the data. + +#### Term Search + +Term search is the only searching primitive exposed in today's bleve.index API. This ultimately could limit our ability to take advantage of the indexing improvements, but it also means it will be easier to get a first version of this working. + +A term search for term T in field F will look something like this: + +``` + searchResultPostings = empty + foreach segment { + dict := segment.Dictionary(F) + segmentResultPostings = dict.PostingsList(T, segmentSnapshotDeleted) + // make segmentLocal numbers into global numbers, and flip bits in searchResultPostings + } +``` + +The searchResultPostings will be a new implementation of the TermFieldReader interface. + +As a reminder this interface is: + +``` +// TermFieldReader is the interface exposing the enumeration of documents +// containing a given term in a given field. Documents are returned in byte +// lexicographic order over their identifiers. +type TermFieldReader interface { + // Next returns the next document containing the term in this field, or nil + // when it reaches the end of the enumeration. The preAlloced TermFieldDoc + // is optional, and when non-nil, will be used instead of allocating memory. + Next(preAlloced *TermFieldDoc) (*TermFieldDoc, error) + + // Advance resets the enumeration at specified document or its immediate + // follower. + Advance(ID IndexInternalID, preAlloced *TermFieldDoc) (*TermFieldDoc, error) + + // Count returns the number of documents contains the term in this field. + Count() uint64 + Close() error +} +``` + +At first glance this appears problematic, we have no way to return documents in order of their identifiers. But it turns out the wording of this perhaps too strong, or a bit ambiguous. Originally, this referred to the external identifiers, but with the introduction of a distinction between internal/external identifiers, returning them in order of their internal identifiers is also acceptable. **ASIDE**: the reason for this is that most callers just use Next() and literally don't care what the order is, they could be in any order and it would be fine. There is only one search that cares and that is the ConjunctionSearcher, which relies on Next/Advance having very specific semantics. Later in this document we will have a proposal to split into multiple interfaces: + +- The weakest interface, only supports Next() no ordering at all. +- Ordered, supporting Advance() +- And/Or'able capable of internally efficiently doing these ops with like interfaces (if not capable then can always fall back to external walking) + +But, the good news is that we don't even have to do that for our first implementation. As long as the global numbers we use for internal identifiers are consistent within this IndexSnapshot, then Next() will be ordered by ascending document number, and Advance() will still work correctly. + +NOTE: there is another place where we rely on the ordering of these hits, and that is in the "\_id" sort order. Previously this was the natural order, and a NOOP for the collector, now it must be implemented by actually sorting on the "\_id" field. We probably should introduce at least a marker interface to detect this. + +An ASCII art example: + +``` +Let's start with the IndexSnapshot we ended with earlier: + +3 - Index Batch [ C' ] + + segment 0 1 2 + numbers [ 1 2 3 ] [ 1 ] [ 1 ] + \_id [ A B C ] [ B ] [ C ] + + Compute bitset segment-0-deleted-by-2: + [ 0 0 1 ] + + OR it with previous ([ 0 1 0 ]) (call it 0-12) + [ 0 1 1 ] + +Compute bitset segment-1-deleted-by-2: + [ 0 0 0 ] + +OR it with previous (nil) + still just nil + + + IndexSnapshot + segments [ 0 1 2 ] + deleted [ 0-12 nil nil ] + +Now let's search for the term 'cat' in the field 'desc' and let's assume that Document C (both versions) would match it. + +Concurrently: + + - Segment 0 + - Get Term Dictionary For Field 'desc' + - From it get Postings List for term 'cat' EXCLUDING 0-12 + - raw segment matches [ 0 0 1 ] but excluding [ 0 1 1 ] gives [ 0 0 0 ] + - Segment 1 + - Get Term Dictionary For Field 'desc' + - From it get Postings List for term 'cat' excluding nil + - [ 0 ] + - Segment 2 + - Get Term Dictionary For Field 'desc' + - From it get Postings List for term 'cat' excluding nil + - [ 1 ] + +Map local bitsets into global number space (global meaning cross-segment but still unique to this snapshot) + +IndexSnapshot already should have mapping something like: +0 - Offset 0 +1 - Offset 3 (because segment 0 had 3 docs) +2 - Offset 4 (because segment 1 had 1 doc) + +This maps to search result bitset: + +[ 0 0 0 0 1] + +Caller would call Next() and get doc number 5 (assuming 1 based indexing for now) + +Caller could then ask to get term locations, stored fields, external doc ID for document number 5. Internally in the IndexSnapshot, we can now convert that back, and realize doc number 5 comes from segment 2, 5-4=1 so we're looking for doc number 1 in segment 2. That happens to be C... + +``` + +#### Future improvements + +In the future, interfaces to detect these non-serially operating TermFieldReaders could expose their own And() and Or() up to the higher level Conjunction/Disjunction searchers. Doing this alone offers some win, but also means there would be greater burden on the Searcher code rewriting logical expressions for maximum performance. + +Another related topic is that of peak memory usage. With serially operating TermFieldReaders it was necessary to start them all at the same time and operate in unison. However, with these non-serially operating TermFieldReaders we have the option of doing a few at a time, consolidating them, dispoting the intermediaries, and then doing a few more. For very complex queries with many clauses this could reduce peak memory usage. + + +### Memory Tracking + +All segments must be able to produce two statistics, an estimate of their explicit memory usage, and their actual size on disk (if any). For in-memory segments, disk usage could be zero, and the memory usage represents the entire information content. For mmap-based disk segments, the memory could be as low as the size of tracking structure itself (say just a few pointers). + +This would allow the implementation to throttle or block incoming mutations when a threshold memory usage has (or would be) exceeded. + +### Persistence + +Obviously, we want to support (but maybe not require) asynchronous persistence of segments. My expectation is that segments are initially built in memory. At some point they are persisted to disk. This poses some interesting challenges. + +At runtime, the state of an index (it's IndexSnapshot) is not only the contents of the segments, but also the bitmasks of deleted documents. These bitmasks indirectly encode an ordering in which the segments were added. The reason is that the bitmasks encode which items have been obsoleted by other (subsequent or more future) segments. In the runtime implementation we compute bitmask deltas and then merge them at the same time we bring the new segment in. One idea is that we could take a similar approach on disk. When we persist a segment, we persist the bitmask deltas of segments known to exist at that time, and eventually these can get merged up into a base segment deleted bitmask. + +This also relates to the topic rollback, addressed next... + + +### Rollback + +One desirable property in the Couchbase ecosystem is the ability to rollback to some previous (though typically not long ago) state. One idea for keeping this property in this design is to protect some of the most recent segments from merging. Then, if necessary, they could be "undone" to reveal previous states of the system. In these scenarios "undone" has to properly undo the deleted bitmasks on the other segments. Again, the current thinking is that rather than "undo" anything, it could be work that was deferred in the first place, thus making it easier to logically undo. + +Another possibly related approach would be to tie this into our existing snapshot mechanism. Perhaps simulating a slow reader (holding onto index snapshots) for some period of time, can be the mechanism to achieve the desired end goal. + + +### Internal Storage + +The bleve.index API has support for "internal storage". The ability to store information under a separate name space. + +This is not used for high volume storage, so it is tempting to think we could just put a small k/v store alongside the rest of the index. But, the reality is that this storage is used to maintain key information related to the rollback scenario. Because of this, its crucial that ordering and overwriting of key/value pairs correspond with actual segment persistence in the index. Based on this, I believe its important to put the internal key/value pairs inside the segments themselves. But, this also means that they must follow a similar "deleted" bitmask approach to obsolete values in older segments. But, this also seems to substantially increase the complexity of the solution because of the separate name space, it would appear to require its own bitmask. Further keys aren't numeric, which then implies yet another mapping from internal key to number, etc. + +More thought is required here. + +### Merging + +The segmented index approach requires merging to prevent the number of segments from growing too large. + +Recent experience with LSMs has taught us that having the correct merge strategy can make a huge difference in the overall performance of the system. In particular, a simple merge strategy which merges segments too aggressively can lead to high write amplification and unnecessarily rendering cached data useless. + +A few simple principles have been identified. + +- Roughly we merge multiple smaller segments into a single larger one. +- The larger a segment gets the less likely we should be to ever merge it. +- Segments with large numbers of deleted/obsoleted items are good candidates as the merge will result in a space savings. +- Segments with all items deleted/obsoleted can be dropped. + +Merging of a segment should be able to proceed even if that segment is held by an ongoing snapshot, it should only delay the removal of it. diff --git a/index/scorch/builder.go b/index/scorch/builder.go new file mode 100644 index 0000000..d4d8e9c --- /dev/null +++ b/index/scorch/builder.go @@ -0,0 +1,332 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "os" + "sync" + + "github.com/RoaringBitmap/roaring/v2" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" + bolt "go.etcd.io/bbolt" +) + +const DefaultBuilderBatchSize = 1000 +const DefaultBuilderMergeMax = 10 + +type Builder struct { + m sync.Mutex + segCount uint64 + path string + buildPath string + segPaths []string + batchSize int + mergeMax int + batch *index.Batch + internal map[string][]byte + segPlugin SegmentPlugin +} + +func NewBuilder(config map[string]interface{}) (*Builder, error) { + path, ok := config["path"].(string) + if !ok { + return nil, fmt.Errorf("must specify path") + } + + buildPathPrefix, _ := config["buildPathPrefix"].(string) + buildPath, err := os.MkdirTemp(buildPathPrefix, "scorch-offline-build") + if err != nil { + return nil, err + } + + rv := &Builder{ + path: path, + buildPath: buildPath, + mergeMax: DefaultBuilderMergeMax, + batchSize: DefaultBuilderBatchSize, + batch: index.NewBatch(), + segPlugin: defaultSegmentPlugin, + } + + err = rv.parseConfig(config) + if err != nil { + return nil, fmt.Errorf("error parsing builder config: %v", err) + } + + return rv, nil +} + +func (o *Builder) parseConfig(config map[string]interface{}) (err error) { + if v, ok := config["mergeMax"]; ok { + var t int + if t, err = parseToInteger(v); err != nil { + return fmt.Errorf("mergeMax parse err: %v", err) + } + if t > 0 { + o.mergeMax = t + } + } + + if v, ok := config["batchSize"]; ok { + var t int + if t, err = parseToInteger(v); err != nil { + return fmt.Errorf("batchSize parse err: %v", err) + } + if t > 0 { + o.batchSize = t + } + } + + if v, ok := config["internal"]; ok { + if vinternal, ok := v.(map[string][]byte); ok { + o.internal = vinternal + } + } + + forcedSegmentType, forcedSegmentVersion, err := configForceSegmentTypeVersion(config) + if err != nil { + return err + } + if forcedSegmentType != "" && forcedSegmentVersion != 0 { + segPlugin, err := chooseSegmentPlugin(forcedSegmentType, + uint32(forcedSegmentVersion)) + if err != nil { + return err + } + o.segPlugin = segPlugin + } + + return nil +} + +// Index will place the document into the index. +// It is invalid to index the same document multiple times. +func (o *Builder) Index(doc index.Document) error { + o.m.Lock() + defer o.m.Unlock() + + o.batch.Update(doc) + + return o.maybeFlushBatchLOCKED(o.batchSize) +} + +func (o *Builder) maybeFlushBatchLOCKED(moreThan int) error { + if len(o.batch.IndexOps) >= moreThan { + defer o.batch.Reset() + return o.executeBatchLOCKED(o.batch) + } + return nil +} + +func (o *Builder) executeBatchLOCKED(batch *index.Batch) (err error) { + analysisResults := make([]index.Document, 0, len(batch.IndexOps)) + for _, doc := range batch.IndexOps { + if doc != nil { + // insert _id field + doc.AddIDField() + // perform analysis directly + analyze(doc, nil) + analysisResults = append(analysisResults, doc) + } + } + + seg, _, err := o.segPlugin.New(analysisResults) + if err != nil { + return fmt.Errorf("error building segment base: %v", err) + } + + filename := zapFileName(o.segCount) + o.segCount++ + path := o.buildPath + string(os.PathSeparator) + filename + + if segUnpersisted, ok := seg.(segment.UnpersistedSegment); ok { + err = segUnpersisted.Persist(path) + if err != nil { + return fmt.Errorf("error persisting segment base to %s: %v", path, err) + } + + o.segPaths = append(o.segPaths, path) + return nil + } + + return fmt.Errorf("new segment does not implement unpersisted: %T", seg) +} + +func (o *Builder) doMerge() error { + // as long as we have more than 1 segment, keep merging + for len(o.segPaths) > 1 { + + // merge the next number of segments into one new one + // or, if there are fewer than remaining, merge them all + mergeCount := o.mergeMax + if mergeCount > len(o.segPaths) { + mergeCount = len(o.segPaths) + } + + mergePaths := o.segPaths[0:mergeCount] + o.segPaths = o.segPaths[mergeCount:] + + // open each of the segments to be merged + mergeSegs := make([]segment.Segment, 0, mergeCount) + + // closeOpenedSegs attempts to close all opened + // segments even if an error occurs, in which case + // the first error is returned + closeOpenedSegs := func() error { + var err error + for _, seg := range mergeSegs { + clErr := seg.Close() + if clErr != nil && err == nil { + err = clErr + } + } + return err + } + + for _, mergePath := range mergePaths { + seg, err := o.segPlugin.Open(mergePath) + if err != nil { + _ = closeOpenedSegs() + return fmt.Errorf("error opening segment (%s) for merge: %v", mergePath, err) + } + mergeSegs = append(mergeSegs, seg) + } + + // do the merge + mergedSegPath := o.buildPath + string(os.PathSeparator) + zapFileName(o.segCount) + drops := make([]*roaring.Bitmap, mergeCount) + _, _, err := o.segPlugin.Merge(mergeSegs, drops, mergedSegPath, nil, nil) + if err != nil { + _ = closeOpenedSegs() + return fmt.Errorf("error merging segments (%v): %v", mergePaths, err) + } + o.segCount++ + o.segPaths = append(o.segPaths, mergedSegPath) + + // close segments opened for merge + err = closeOpenedSegs() + if err != nil { + return fmt.Errorf("error closing opened segments: %v", err) + } + + // remove merged segments + for _, mergePath := range mergePaths { + err = os.RemoveAll(mergePath) + if err != nil { + return fmt.Errorf("error removing segment %s after merge: %v", mergePath, err) + } + } + } + + return nil +} + +func (o *Builder) Close() error { + o.m.Lock() + defer o.m.Unlock() + + // see if there is a partial batch + err := o.maybeFlushBatchLOCKED(1) + if err != nil { + return fmt.Errorf("error flushing batch before close: %v", err) + } + + // perform all the merging + err = o.doMerge() + if err != nil { + return fmt.Errorf("error while merging: %v", err) + } + + // ensure the store path exists + err = os.MkdirAll(o.path, 0700) + if err != nil { + return err + } + + // move final segment into place + // segment id 2 is chosen to match the behavior of a scorch + // index which indexes a single batch of data + finalSegPath := o.path + string(os.PathSeparator) + zapFileName(2) + err = os.Rename(o.segPaths[0], finalSegPath) + if err != nil { + return fmt.Errorf("error moving final segment into place: %v", err) + } + + // remove the buildPath, as it is no longer needed + err = os.RemoveAll(o.buildPath) + if err != nil { + return fmt.Errorf("error removing build path: %v", err) + } + + // prepare wrapping + seg, err := o.segPlugin.Open(finalSegPath) + if err != nil { + return fmt.Errorf("error opening final segment") + } + + // create a segment snapshot for this segment + ss := &SegmentSnapshot{ + segment: seg, + } + is := &IndexSnapshot{ + epoch: 3, // chosen to match scorch behavior when indexing a single batch + segment: []*SegmentSnapshot{ss}, + creator: "scorch-builder", + internal: o.internal, + } + + // create the root bolt + rootBoltPath := o.path + string(os.PathSeparator) + "root.bolt" + rootBolt, err := bolt.Open(rootBoltPath, 0600, nil) + if err != nil { + return err + } + + // start a write transaction + tx, err := rootBolt.Begin(true) + if err != nil { + return err + } + + // fill the root bolt with this fake index snapshot + _, _, err = prepareBoltSnapshot(is, tx, o.path, o.segPlugin, nil, nil) + if err != nil { + _ = tx.Rollback() + _ = rootBolt.Close() + return fmt.Errorf("error preparing bolt snapshot in root.bolt: %v", err) + } + + // commit bolt data + err = tx.Commit() + if err != nil { + _ = rootBolt.Close() + return fmt.Errorf("error committing bolt tx in root.bolt: %v", err) + } + + // close bolt + err = rootBolt.Close() + if err != nil { + return fmt.Errorf("error closing root.bolt: %v", err) + } + + // close final segment + err = seg.Close() + if err != nil { + return fmt.Errorf("error closing final segment: %v", err) + } + return nil +} diff --git a/index/scorch/builder_test.go b/index/scorch/builder_test.go new file mode 100644 index 0000000..3fac77c --- /dev/null +++ b/index/scorch/builder_test.go @@ -0,0 +1,159 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +func TestBuilder(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "scorch-builder-test") + if err != nil { + t.Fatal(err) + } + defer func() { + err = os.RemoveAll(tmpDir) + if err != nil { + t.Fatalf("error cleaning up test index: %v", err) + } + }() + options := map[string]interface{}{ + "path": tmpDir, + "batchSize": 2, + "mergeMax": 2, + } + b, err := NewBuilder(options) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 10; i++ { + doc := document.NewDocument(fmt.Sprintf("%d", i)) + doc.AddField(document.NewTextField("name", nil, []byte("hello"))) + err = b.Index(doc) + if err != nil { + t.Fatal(err) + } + } + + err = b.Close() + if err != nil { + t.Fatal(err) + } + + checkIndex(t, tmpDir, []byte("hello"), "name", 10) +} + +func checkIndex(t *testing.T, path string, term []byte, field string, expectCount int) { + cfg := make(map[string]interface{}) + cfg["path"] = path + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatalf("error closing index: %v", err) + } + }() + + r, err := idx.Reader() + if err != nil { + t.Fatalf("error accessing index reader: %v", err) + } + defer func() { + err = r.Close() + if err != nil { + t.Fatalf("error closing reader: %v", err) + } + }() + + // check the count, expect 10 docs + count, err := r.DocCount() + if err != nil { + t.Errorf("error accessing index doc count: %v", err) + } else if count != uint64(expectCount) { + t.Errorf("expected %d docs, got %d", expectCount, count) + } + + // run a search for hello + tfr, err := r.TermFieldReader(context.TODO(), term, field, false, false, false) + if err != nil { + t.Errorf("error accessing term field reader: %v", err) + } else { + var rows int + tfd, err := tfr.Next(nil) + for err == nil && tfd != nil { + rows++ + tfd, err = tfr.Next(nil) + } + if err != nil { + t.Errorf("error calling next on term field reader: %v", err) + } + if rows != expectCount { + t.Errorf("expected %d rows for term hello, field name, got %d", expectCount, rows) + } + } +} + +func TestBuilderFlushFinalBatch(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "scorch-builder-test") + if err != nil { + t.Fatal(err) + } + defer func() { + err = os.RemoveAll(tmpDir) + if err != nil { + t.Fatalf("error cleaning up test index: %v", err) + } + }() + options := map[string]interface{}{ + "path": tmpDir, + "batchSize": 2, + "mergeMax": 2, + } + b, err := NewBuilder(options) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 9; i++ { + doc := document.NewDocument(fmt.Sprintf("%d", i)) + doc.AddField(document.NewTextField("name", nil, []byte("hello"))) + err = b.Index(doc) + if err != nil { + t.Fatal(err) + } + } + + err = b.Close() + if err != nil { + t.Fatal(err) + } + + checkIndex(t, tmpDir, []byte("hello"), "name", 9) +} diff --git a/index/scorch/empty.go b/index/scorch/empty.go new file mode 100644 index 0000000..34619d4 --- /dev/null +++ b/index/scorch/empty.go @@ -0,0 +1,41 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import segment "github.com/blevesearch/scorch_segment_api/v2" + +type emptyPostingsIterator struct{} + +func (e *emptyPostingsIterator) Next() (segment.Posting, error) { + return nil, nil +} + +func (e *emptyPostingsIterator) Advance(uint64) (segment.Posting, error) { + return nil, nil +} + +func (e *emptyPostingsIterator) Size() int { + return 0 +} + +func (e *emptyPostingsIterator) BytesRead() uint64 { + return 0 +} + +func (e *emptyPostingsIterator) ResetBytesRead(uint64) {} + +func (e *emptyPostingsIterator) BytesWritten() uint64 { return 0 } + +var anEmptyPostingsIterator = &emptyPostingsIterator{} diff --git a/index/scorch/event.go b/index/scorch/event.go new file mode 100644 index 0000000..b17c7f4 --- /dev/null +++ b/index/scorch/event.go @@ -0,0 +1,77 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import "time" + +// RegistryAsyncErrorCallbacks should be treated as read-only after +// process init()'ialization. +var RegistryAsyncErrorCallbacks = map[string]func(error, string){} + +// RegistryEventCallbacks should be treated as read-only after +// process init()'ialization. +// In the event of not having a callback, these return true. +var RegistryEventCallbacks = map[string]func(Event) bool{} + +// Event represents the information provided in an OnEvent() callback. +type Event struct { + Kind EventKind + Scorch *Scorch + Duration time.Duration +} + +// EventKind represents an event code for OnEvent() callbacks. +type EventKind int + +// EventKindCloseStart is fired when a Scorch.Close() has begun. +var EventKindCloseStart = EventKind(1) + +// EventKindClose is fired when a scorch index has been fully closed. +var EventKindClose = EventKind(2) + +// EventKindMergerProgress is fired when the merger has completed a +// round of merge processing. +var EventKindMergerProgress = EventKind(3) + +// EventKindPersisterProgress is fired when the persister has completed +// a round of persistence processing. +var EventKindPersisterProgress = EventKind(4) + +// EventKindBatchIntroductionStart is fired when Batch() is invoked which +// introduces a new segment. +var EventKindBatchIntroductionStart = EventKind(5) + +// EventKindBatchIntroduction is fired when Batch() completes. +var EventKindBatchIntroduction = EventKind(6) + +// EventKindMergeTaskIntroductionStart is fired when the merger is about to +// start the introduction of merged segment from a single merge task. +var EventKindMergeTaskIntroductionStart = EventKind(7) + +// EventKindMergeTaskIntroduction is fired when the merger has completed +// the introduction of merged segment from a single merge task. +var EventKindMergeTaskIntroduction = EventKind(8) + +// EventKindPreMergeCheck is fired before the merge begins to check if +// the caller should proceed with the merge. +var EventKindPreMergeCheck = EventKind(9) + +// EventKindIndexStart is fired when Index() is invoked which +// creates a new Document object from an interface using the index mapping. +var EventKindIndexStart = EventKind(10) + +// EventKindPurgerCheck is fired before the purge code is invoked and decides +// whether to execute or not. For unit test purposes +var EventKindPurgerCheck = EventKind(11) diff --git a/index/scorch/event_test.go b/index/scorch/event_test.go new file mode 100644 index 0000000..be163a9 --- /dev/null +++ b/index/scorch/event_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +func TestEventBatchIntroductionStart(t *testing.T) { + testConfig := CreateConfig("TestEventBatchIntroductionStart") + err := InitTest(testConfig) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(testConfig) + if err != nil { + t.Fatal(err) + } + }() + + var count int + RegistryEventCallbacks["test"] = func(e Event) bool { + if e.Kind == EventKindBatchIntroductionStart { + count++ + } + return true + } + + ourConfig := make(map[string]interface{}, len(testConfig)) + for k, v := range testConfig { + ourConfig[k] = v + } + ourConfig["eventCallbackName"] = "test" + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, ourConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + if count != 1 { + t.Fatalf("expected to see 1 batch introduction event event, saw %d", count) + } +} diff --git a/index/scorch/field_dict_test.go b/index/scorch/field_dict_test.go new file mode 100644 index 0000000..44afe26 --- /dev/null +++ b/index/scorch/field_dict_test.go @@ -0,0 +1,186 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +func TestIndexFieldDict(t *testing.T) { + cfg := CreateConfig("TestIndexFieldDict") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + cerr := idx.Close() + if cerr != nil { + t.Fatal(cerr) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("2") + doc.AddField(document.NewTextFieldWithAnalyzer("name", []uint64{}, []byte("test test test"), testAnalyzer)) + doc.AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("eat more rice"), index.IndexField|index.IncludeTermVectors, testAnalyzer)) + doc.AddField(document.NewTextFieldCustom("prefix", []uint64{}, []byte("bob cat cats catting dog doggy zoo"), index.IndexField|index.IncludeTermVectors, testAnalyzer)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + dict, err := indexReader.FieldDict("name") + if err != nil { + t.Errorf("error creating reader: %v", err) + } + defer func() { + err := dict.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount := 0 + curr, err := dict.Next() + for err == nil && curr != nil { + termCount++ + if curr.Term != "test" { + t.Errorf("expected term to be 'test', got '%s'", curr.Term) + } + curr, err = dict.Next() + } + if termCount != 1 { + t.Errorf("expected 1 term for this field, got %d", termCount) + } + + dict2, err := indexReader.FieldDict("desc") + if err != nil { + t.Fatalf("error creating reader: %v", err) + } + defer func() { + err := dict2.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount = 0 + terms := make([]string, 0) + curr, err = dict2.Next() + for err == nil && curr != nil { + termCount++ + terms = append(terms, curr.Term) + curr, err = dict2.Next() + } + if termCount != 3 { + t.Errorf("expected 3 term for this field, got %d", termCount) + } + expectedTerms := []string{"eat", "more", "rice"} + if !reflect.DeepEqual(expectedTerms, terms) { + t.Errorf("expected %#v, got %#v", expectedTerms, terms) + } + // test start and end range + dict3, err := indexReader.FieldDictRange("desc", []byte("fun"), []byte("nice")) + if err != nil { + t.Errorf("error creating reader: %v", err) + } + defer func() { + err := dict3.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount = 0 + terms = make([]string, 0) + curr, err = dict3.Next() + for err == nil && curr != nil { + termCount++ + terms = append(terms, curr.Term) + curr, err = dict3.Next() + } + if termCount != 1 { + t.Errorf("expected 1 term for this field, got %d", termCount) + } + expectedTerms = []string{"more"} + if !reflect.DeepEqual(expectedTerms, terms) { + t.Errorf("expected %#v, got %#v", expectedTerms, terms) + } + + // test use case for prefix + dict4, err := indexReader.FieldDictPrefix("prefix", []byte("cat")) + if err != nil { + t.Errorf("error creating reader: %v", err) + } + defer func() { + err := dict4.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount = 0 + terms = make([]string, 0) + curr, err = dict4.Next() + for err == nil && curr != nil { + termCount++ + terms = append(terms, curr.Term) + curr, err = dict4.Next() + } + if termCount != 3 { + t.Errorf("expected 3 term for this field, got %d", termCount) + } + expectedTerms = []string{"cat", "cats", "catting"} + if !reflect.DeepEqual(expectedTerms, terms) { + t.Errorf("expected %#v, got %#v", expectedTerms, terms) + } +} diff --git a/index/scorch/int.go b/index/scorch/int.go new file mode 100644 index 0000000..4fa6d7f --- /dev/null +++ b/index/scorch/int.go @@ -0,0 +1,92 @@ +// Copyright 2014 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +// This code originated from: +// https://github.com/cockroachdb/cockroach/blob/2dd65dde5d90c157f4b93f92502ca1063b904e1d/pkg/util/encoding/encoding.go + +// Modified to not use pkg/errors + +package scorch + +import "fmt" + +const ( + // intMin is chosen such that the range of int tags does not overlap the + // ascii character set that is frequently used in testing. + intMin = 0x80 // 128 + intMaxWidth = 8 + intZero = intMin + intMaxWidth // 136 + intSmall = intMax - intZero - intMaxWidth // 109 + // intMax is the maximum int tag value. + intMax = 0xfd // 253 +) + +// encodeUvarintAscending encodes the uint64 value using a variable length +// (length-prefixed) representation. The length is encoded as a single +// byte indicating the number of encoded bytes (-8) to follow. See +// EncodeVarintAscending for rationale. The encoded bytes are appended to the +// supplied buffer and the final buffer is returned. +func encodeUvarintAscending(b []byte, v uint64) []byte { + switch { + case v <= intSmall: + return append(b, intZero+byte(v)) + case v <= 0xff: + return append(b, intMax-7, byte(v)) + case v <= 0xffff: + return append(b, intMax-6, byte(v>>8), byte(v)) + case v <= 0xffffff: + return append(b, intMax-5, byte(v>>16), byte(v>>8), byte(v)) + case v <= 0xffffffff: + return append(b, intMax-4, byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) + case v <= 0xffffffffff: + return append(b, intMax-3, byte(v>>32), byte(v>>24), byte(v>>16), byte(v>>8), + byte(v)) + case v <= 0xffffffffffff: + return append(b, intMax-2, byte(v>>40), byte(v>>32), byte(v>>24), byte(v>>16), + byte(v>>8), byte(v)) + case v <= 0xffffffffffffff: + return append(b, intMax-1, byte(v>>48), byte(v>>40), byte(v>>32), byte(v>>24), + byte(v>>16), byte(v>>8), byte(v)) + default: + return append(b, intMax, byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), + byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) + } +} + +// decodeUvarintAscending decodes a varint encoded uint64 from the input +// buffer. The remainder of the input buffer and the decoded uint64 +// are returned. +func decodeUvarintAscending(b []byte) ([]byte, uint64, error) { + if len(b) == 0 { + return nil, 0, fmt.Errorf("insufficient bytes to decode uvarint value") + } + length := int(b[0]) - intZero + b = b[1:] // skip length byte + if length <= intSmall { + return b, uint64(length), nil + } + length -= intSmall + if length < 0 || length > 8 { + return nil, 0, fmt.Errorf("invalid uvarint length of %d", length) + } else if len(b) < length { + return nil, 0, fmt.Errorf("insufficient bytes to decode uvarint value: %q", b) + } + var v uint64 + // It is faster to range over the elements in a slice than to index + // into the slice on each loop iteration. + for _, t := range b[:length] { + v = (v << 8) | uint64(t) + } + return b[length:], v, nil +} diff --git a/index/scorch/int_test.go b/index/scorch/int_test.go new file mode 100644 index 0000000..202b157 --- /dev/null +++ b/index/scorch/int_test.go @@ -0,0 +1,96 @@ +// Copyright 2014 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +// This code originated from: +// https://github.com/cockroachdb/cockroach/blob/2dd65dde5d90c157f4b93f92502ca1063b904e1d/pkg/util/encoding/encoding_test.go + +// Modified to only test the parts we borrowed + +package scorch + +import ( + "bytes" + "math" + "testing" +) + +type testCaseUint64 struct { + value uint64 + expEnc []byte +} + +func TestEncodeDecodeUvarint(t *testing.T) { + testBasicEncodeDecodeUint64(encodeUvarintAscending, decodeUvarintAscending, false, t) + testCases := []testCaseUint64{ + {0, []byte{0x88}}, + {1, []byte{0x89}}, + {109, []byte{0xf5}}, + {110, []byte{0xf6, 0x6e}}, + {1 << 8, []byte{0xf7, 0x01, 0x00}}, + {math.MaxUint64, []byte{0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}, + } + testCustomEncodeUint64(testCases, encodeUvarintAscending, t) +} + +func testBasicEncodeDecodeUint64( + encFunc func([]byte, uint64) []byte, + decFunc func([]byte) ([]byte, uint64, error), + descending bool, t *testing.T, +) { + testCases := []uint64{ + 0, 1, + 1<<8 - 1, 1 << 8, + 1<<16 - 1, 1 << 16, + 1<<24 - 1, 1 << 24, + 1<<32 - 1, 1 << 32, + 1<<40 - 1, 1 << 40, + 1<<48 - 1, 1 << 48, + 1<<56 - 1, 1 << 56, + math.MaxUint64 - 1, math.MaxUint64, + } + + var lastEnc []byte + for i, v := range testCases { + enc := encFunc(nil, v) + if i > 0 { + if (descending && bytes.Compare(enc, lastEnc) >= 0) || + (!descending && bytes.Compare(enc, lastEnc) < 0) { + t.Errorf("ordered constraint violated for %d: [% x] vs. [% x]", v, enc, lastEnc) + } + } + b, decode, err := decFunc(enc) + if err != nil { + t.Error(err) + continue + } + if len(b) != 0 { + t.Errorf("leftover bytes: [% x]", b) + } + if decode != v { + t.Errorf("decode yielded different value than input: %d vs. %d", decode, v) + } + lastEnc = enc + } +} + +func testCustomEncodeUint64( + testCases []testCaseUint64, encFunc func([]byte, uint64) []byte, t *testing.T, +) { + for _, test := range testCases { + enc := encFunc(nil, test.value) + if !bytes.Equal(enc, test.expEnc) { + t.Errorf("expected [% x]; got [% x] (value: %d)", test.expEnc, enc, test.value) + } + } +} diff --git a/index/scorch/introducer.go b/index/scorch/introducer.go new file mode 100644 index 0000000..209da5b --- /dev/null +++ b/index/scorch/introducer.go @@ -0,0 +1,515 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "path/filepath" + "sync/atomic" + + "github.com/RoaringBitmap/roaring/v2" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +type segmentIntroduction struct { + id uint64 + data segment.Segment + obsoletes map[uint64]*roaring.Bitmap + ids []string + internal map[string][]byte + stats *fieldStats + + applied chan error + persisted chan error + persistedCallback index.BatchCallback +} + +type persistIntroduction struct { + persisted map[uint64]segment.Segment + applied notificationChan +} + +type epochWatcher struct { + epoch uint64 + notifyCh notificationChan +} + +func (s *Scorch) introducerLoop() { + defer func() { + if r := recover(); r != nil { + s.fireAsyncError(&AsyncPanicError{ + Source: "introducer", + Path: s.path, + }) + } + + s.asyncTasks.Done() + }() + + var epochWatchers []*epochWatcher +OUTER: + for { + atomic.AddUint64(&s.stats.TotIntroduceLoop, 1) + + select { + case <-s.closeCh: + break OUTER + + case epochWatcher := <-s.introducerNotifier: + epochWatchers = append(epochWatchers, epochWatcher) + + case nextMerge := <-s.merges: + s.introduceMerge(nextMerge) + + case next := <-s.introductions: + err := s.introduceSegment(next) + if err != nil { + continue OUTER + } + + case persist := <-s.persists: + s.introducePersist(persist) + + } + + var epochCurr uint64 + s.rootLock.RLock() + if s.root != nil { + epochCurr = s.root.epoch + } + s.rootLock.RUnlock() + var epochWatchersNext []*epochWatcher + for _, w := range epochWatchers { + if w.epoch < epochCurr { + close(w.notifyCh) + } else { + epochWatchersNext = append(epochWatchersNext, w) + } + } + epochWatchers = epochWatchersNext + } +} + +func (s *Scorch) introduceSegment(next *segmentIntroduction) error { + atomic.AddUint64(&s.stats.TotIntroduceSegmentBeg, 1) + defer atomic.AddUint64(&s.stats.TotIntroduceSegmentEnd, 1) + + s.rootLock.RLock() + root := s.root + root.AddRef() + s.rootLock.RUnlock() + + defer func() { _ = root.DecRef() }() + + nsegs := len(root.segment) + + // prepare new index snapshot + newSnapshot := &IndexSnapshot{ + parent: s, + segment: make([]*SegmentSnapshot, 0, nsegs+1), + offsets: make([]uint64, 0, nsegs+1), + internal: make(map[string][]byte, len(root.internal)), + refs: 1, + creator: "introduceSegment", + } + + // iterate through current segments + var running uint64 + var docsToPersistCount, memSegments, fileSegments uint64 + var droppedSegmentFiles []string + for i := range root.segment { + // see if optimistic work included this segment + delta, ok := next.obsoletes[root.segment[i].id] + if !ok { + var err error + delta, err = root.segment[i].segment.DocNumbers(next.ids) + if err != nil { + next.applied <- fmt.Errorf("error computing doc numbers: %v", err) + close(next.applied) + _ = newSnapshot.DecRef() + return err + } + } + + newss := &SegmentSnapshot{ + id: root.segment[i].id, + segment: root.segment[i].segment, + stats: root.segment[i].stats, + cachedDocs: root.segment[i].cachedDocs, + cachedMeta: root.segment[i].cachedMeta, + creator: root.segment[i].creator, + } + + // apply new obsoletions + if root.segment[i].deleted == nil { + newss.deleted = delta + } else { + if delta.IsEmpty() { + newss.deleted = root.segment[i].deleted + } else { + newss.deleted = roaring.Or(root.segment[i].deleted, delta) + } + } + if newss.deleted.IsEmpty() { + newss.deleted = nil + } + + // check for live size before copying + if newss.LiveSize() > 0 { + newSnapshot.segment = append(newSnapshot.segment, newss) + root.segment[i].segment.AddRef() + newSnapshot.offsets = append(newSnapshot.offsets, running) + running += newss.segment.Count() + } else if seg, ok := newss.segment.(segment.PersistedSegment); ok { + droppedSegmentFiles = append(droppedSegmentFiles, + filepath.Base(seg.Path())) + } + + if isMemorySegment(root.segment[i]) { + docsToPersistCount += root.segment[i].Count() + memSegments++ + } else { + fileSegments++ + } + } + + atomic.StoreUint64(&s.stats.TotItemsToPersist, docsToPersistCount) + atomic.StoreUint64(&s.stats.TotMemorySegmentsAtRoot, memSegments) + atomic.StoreUint64(&s.stats.TotFileSegmentsAtRoot, fileSegments) + + // append new segment, if any, to end of the new index snapshot + if next.data != nil { + newSegmentSnapshot := &SegmentSnapshot{ + id: next.id, + segment: next.data, // take ownership of next.data's ref-count + stats: next.stats, + cachedDocs: &cachedDocs{cache: nil}, + cachedMeta: &cachedMeta{meta: nil}, + creator: "introduceSegment", + } + newSnapshot.segment = append(newSnapshot.segment, newSegmentSnapshot) + newSnapshot.offsets = append(newSnapshot.offsets, running) + + // increment numItemsIntroduced which tracks the number of items + // queued for persistence. + atomic.AddUint64(&s.stats.TotIntroducedItems, newSegmentSnapshot.Count()) + atomic.AddUint64(&s.stats.TotIntroducedSegmentsBatch, 1) + } + // copy old values + for key, oldVal := range root.internal { + newSnapshot.internal[key] = oldVal + } + // set new values and apply deletes + for key, newVal := range next.internal { + if newVal != nil { + newSnapshot.internal[key] = newVal + } else { + delete(newSnapshot.internal, key) + } + } + + newSnapshot.updateSize() + s.rootLock.Lock() + if next.persisted != nil { + s.rootPersisted = append(s.rootPersisted, next.persisted) + } + if next.persistedCallback != nil { + s.persistedCallbacks = append(s.persistedCallbacks, next.persistedCallback) + } + // swap in new index snapshot + newSnapshot.epoch = s.nextSnapshotEpoch + s.nextSnapshotEpoch++ + rootPrev := s.root + s.root = newSnapshot + atomic.StoreUint64(&s.stats.CurRootEpoch, s.root.epoch) + // release lock + s.rootLock.Unlock() + + if rootPrev != nil { + _ = rootPrev.DecRef() + } + + // update the removal eligibility for those segment files + // that are not a part of the latest root. + for _, filename := range droppedSegmentFiles { + s.unmarkIneligibleForRemoval(filename) + } + + close(next.applied) + + return nil +} + +func (s *Scorch) introducePersist(persist *persistIntroduction) { + atomic.AddUint64(&s.stats.TotIntroducePersistBeg, 1) + defer atomic.AddUint64(&s.stats.TotIntroducePersistEnd, 1) + + s.rootLock.Lock() + root := s.root + root.AddRef() + nextSnapshotEpoch := s.nextSnapshotEpoch + s.nextSnapshotEpoch++ + s.rootLock.Unlock() + + defer func() { _ = root.DecRef() }() + + newIndexSnapshot := &IndexSnapshot{ + parent: s, + epoch: nextSnapshotEpoch, + segment: make([]*SegmentSnapshot, len(root.segment)), + offsets: make([]uint64, len(root.offsets)), + internal: make(map[string][]byte, len(root.internal)), + refs: 1, + creator: "introducePersist", + } + + var docsToPersistCount, memSegments, fileSegments uint64 + for i, segmentSnapshot := range root.segment { + // see if this segment has been replaced + if replacement, ok := persist.persisted[segmentSnapshot.id]; ok { + newSegmentSnapshot := &SegmentSnapshot{ + id: segmentSnapshot.id, + segment: replacement, + deleted: segmentSnapshot.deleted, + stats: segmentSnapshot.stats, + cachedDocs: segmentSnapshot.cachedDocs, + cachedMeta: segmentSnapshot.cachedMeta, + creator: "introducePersist", + mmaped: 1, + } + newIndexSnapshot.segment[i] = newSegmentSnapshot + delete(persist.persisted, segmentSnapshot.id) + + // update items persisted incase of a new segment snapshot + atomic.AddUint64(&s.stats.TotPersistedItems, newSegmentSnapshot.Count()) + atomic.AddUint64(&s.stats.TotPersistedSegments, 1) + fileSegments++ + } else { + newIndexSnapshot.segment[i] = root.segment[i] + newIndexSnapshot.segment[i].segment.AddRef() + + if isMemorySegment(root.segment[i]) { + docsToPersistCount += root.segment[i].Count() + memSegments++ + } else { + fileSegments++ + } + } + newIndexSnapshot.offsets[i] = root.offsets[i] + } + + for k, v := range root.internal { + newIndexSnapshot.internal[k] = v + } + + atomic.StoreUint64(&s.stats.TotItemsToPersist, docsToPersistCount) + atomic.StoreUint64(&s.stats.TotMemorySegmentsAtRoot, memSegments) + atomic.StoreUint64(&s.stats.TotFileSegmentsAtRoot, fileSegments) + newIndexSnapshot.updateSize() + s.rootLock.Lock() + rootPrev := s.root + s.root = newIndexSnapshot + atomic.StoreUint64(&s.stats.CurRootEpoch, s.root.epoch) + s.rootLock.Unlock() + + if rootPrev != nil { + _ = rootPrev.DecRef() + } + + close(persist.applied) +} + +// The introducer should definitely handle the segmentMerge.notify +// channel before exiting the introduceMerge. +func (s *Scorch) introduceMerge(nextMerge *segmentMerge) { + atomic.AddUint64(&s.stats.TotIntroduceMergeBeg, 1) + defer atomic.AddUint64(&s.stats.TotIntroduceMergeEnd, 1) + + s.rootLock.RLock() + root := s.root + root.AddRef() + s.rootLock.RUnlock() + + defer func() { _ = root.DecRef() }() + + newSnapshot := &IndexSnapshot{ + parent: s, + internal: root.internal, + refs: 1, + creator: "introduceMerge", + } + + var running, docsToPersistCount, memSegments, fileSegments uint64 + var droppedSegmentFiles []string + newSegmentDeleted := make([]*roaring.Bitmap, len(nextMerge.new)) + for i := range newSegmentDeleted { + // create a bitmaps to track the obsoletes per newly merged segments + newSegmentDeleted[i] = roaring.NewBitmap() + } + + // iterate through current segments + for i := range root.segment { + segmentID := root.segment[i].id + if segSnapAtMerge, ok := nextMerge.mergedSegHistory[segmentID]; ok { + // this segment is going away, see if anything else was deleted since we started the merge + if segSnapAtMerge != nil && root.segment[i].deleted != nil { + // assume all these deletes are new + deletedSince := root.segment[i].deleted + // if we already knew about some of them, remove + if segSnapAtMerge.oldSegment.deleted != nil { + deletedSince = roaring.AndNot(root.segment[i].deleted, segSnapAtMerge.oldSegment.deleted) + } + deletedSinceItr := deletedSince.Iterator() + for deletedSinceItr.HasNext() { + oldDocNum := deletedSinceItr.Next() + newDocNum := segSnapAtMerge.oldNewDocIDs[oldDocNum] + newSegmentDeleted[segSnapAtMerge.workerID].Add(uint32(newDocNum)) + } + } + + // clean up the old segment map to figure out the + // obsolete segments wrt root in meantime, whatever + // segments left behind in old map after processing + // the root segments would be the obsolete segment set + delete(nextMerge.mergedSegHistory, segmentID) + } else if root.segment[i].LiveSize() > 0 { + // this segment is staying + newSnapshot.segment = append(newSnapshot.segment, &SegmentSnapshot{ + id: root.segment[i].id, + segment: root.segment[i].segment, + deleted: root.segment[i].deleted, + stats: root.segment[i].stats, + cachedDocs: root.segment[i].cachedDocs, + cachedMeta: root.segment[i].cachedMeta, + creator: root.segment[i].creator, + }) + root.segment[i].segment.AddRef() + newSnapshot.offsets = append(newSnapshot.offsets, running) + running += root.segment[i].segment.Count() + + if isMemorySegment(root.segment[i]) { + docsToPersistCount += root.segment[i].Count() + memSegments++ + } else { + fileSegments++ + } + } else if root.segment[i].LiveSize() == 0 { + if seg, ok := root.segment[i].segment.(segment.PersistedSegment); ok { + droppedSegmentFiles = append(droppedSegmentFiles, + filepath.Base(seg.Path())) + } + } + } + // before the newMerge introduction, need to clean the newly + // merged segment wrt the current root segments, hence + // applying the obsolete segment contents to newly merged segment + for _, ss := range nextMerge.mergedSegHistory { + obsoleted := ss.oldSegment.DocNumbersLive() + if obsoleted != nil { + obsoletedIter := obsoleted.Iterator() + for obsoletedIter.HasNext() { + oldDocNum := obsoletedIter.Next() + newDocNum := ss.oldNewDocIDs[oldDocNum] + newSegmentDeleted[ss.workerID].Add(uint32(newDocNum)) + } + } + } + + skipped := true + // make the newly merged segments part of the newSnapshot being constructed + for i, newMergedSegment := range nextMerge.new { + // checking if this newly merged segment is worth keeping based on + // obsoleted doc count since the merge intro started + if newMergedSegment != nil && + newMergedSegment.Count() > newSegmentDeleted[i].GetCardinality() { + stats := newFieldStats() + if fsr, ok := newMergedSegment.(segment.FieldStatsReporter); ok { + fsr.UpdateFieldStats(stats) + } + + // put the merged segment at the end of newSnapshot + newSnapshot.segment = append(newSnapshot.segment, &SegmentSnapshot{ + id: nextMerge.id[i], + segment: newMergedSegment, // take ownership for nextMerge.new's ref-count + deleted: newSegmentDeleted[i], + stats: stats, + cachedDocs: &cachedDocs{cache: nil}, + cachedMeta: &cachedMeta{meta: nil}, + creator: "introduceMerge", + mmaped: nextMerge.mmaped, + }) + newSnapshot.offsets = append(newSnapshot.offsets, running) + running += newMergedSegment.Count() + + switch newMergedSegment.(type) { + case segment.PersistedSegment: + fileSegments++ + default: + docsToPersistCount += newMergedSegment.Count() - newSegmentDeleted[i].GetCardinality() + memSegments++ + } + skipped = false + } + } + + if skipped { + atomic.AddUint64(&s.stats.TotFileMergeIntroductionsObsoleted, 1) + } else { + atomic.AddUint64(&s.stats.TotIntroducedSegmentsMerge, uint64(len(nextMerge.new))) + } + + atomic.StoreUint64(&s.stats.TotItemsToPersist, docsToPersistCount) + atomic.StoreUint64(&s.stats.TotMemorySegmentsAtRoot, memSegments) + atomic.StoreUint64(&s.stats.TotFileSegmentsAtRoot, fileSegments) + + newSnapshot.AddRef() // 1 ref for the nextMerge.notify response + + newSnapshot.updateSize() + s.rootLock.Lock() + // swap in new index snapshot + newSnapshot.epoch = s.nextSnapshotEpoch + s.nextSnapshotEpoch++ + rootPrev := s.root + s.root = newSnapshot + atomic.StoreUint64(&s.stats.CurRootEpoch, s.root.epoch) + // release lock + s.rootLock.Unlock() + + if rootPrev != nil { + _ = rootPrev.DecRef() + } + + // update the removal eligibility for those segment files + // that are not a part of the latest root. + for _, filename := range droppedSegmentFiles { + s.unmarkIneligibleForRemoval(filename) + } + + // notify requester that we incorporated this + nextMerge.notifyCh <- &mergeTaskIntroStatus{ + indexSnapshot: newSnapshot, + skipped: skipped} + close(nextMerge.notifyCh) +} + +func isMemorySegment(s *SegmentSnapshot) bool { + switch s.segment.(type) { + case segment.PersistedSegment: + return false + default: + return true + } +} diff --git a/index/scorch/merge.go b/index/scorch/merge.go new file mode 100644 index 0000000..9abcf2d --- /dev/null +++ b/index/scorch/merge.go @@ -0,0 +1,637 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/RoaringBitmap/roaring/v2" + "github.com/blevesearch/bleve/v2/index/scorch/mergeplan" + "github.com/blevesearch/bleve/v2/util" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +func (s *Scorch) mergerLoop() { + defer func() { + if r := recover(); r != nil { + s.fireAsyncError(&AsyncPanicError{ + Source: "merger", + Path: s.path, + }) + } + + s.asyncTasks.Done() + }() + + var lastEpochMergePlanned uint64 + var ctrlMsg *mergerCtrl + mergePlannerOptions, err := s.parseMergePlannerOptions() + if err != nil { + s.fireAsyncError(fmt.Errorf("mergePlannerOption json parsing err: %v", err)) + return + } + ctrlMsgDflt := &mergerCtrl{ctx: context.Background(), + options: mergePlannerOptions, + doneCh: nil} + +OUTER: + for { + atomic.AddUint64(&s.stats.TotFileMergeLoopBeg, 1) + + select { + case <-s.closeCh: + break OUTER + + default: + // check to see if there is a new snapshot to persist + s.rootLock.Lock() + ourSnapshot := s.root + ourSnapshot.AddRef() + atomic.StoreUint64(&s.iStats.mergeSnapshotSize, uint64(ourSnapshot.Size())) + atomic.StoreUint64(&s.iStats.mergeEpoch, ourSnapshot.epoch) + s.rootLock.Unlock() + + if ctrlMsg == nil && ourSnapshot.epoch != lastEpochMergePlanned { + ctrlMsg = ctrlMsgDflt + } + if ctrlMsg != nil { + continueMerge := s.fireEvent(EventKindPreMergeCheck, 0) + // The default, if there's no handler, is to continue the merge. + if !continueMerge { + // If it's decided that this merge can't take place now, + // begin the merge process all over again. + // Retry instead of blocking/waiting here since a long wait + // can result in more segments introduced i.e. s.root will + // be updated. + + // decrement the ref count since its no longer needed in this + // iteration + _ = ourSnapshot.DecRef() + continue OUTER + } + + startTime := time.Now() + + // lets get started + err := s.planMergeAtSnapshot(ctrlMsg.ctx, ctrlMsg.options, + ourSnapshot) + if err != nil { + atomic.StoreUint64(&s.iStats.mergeEpoch, 0) + if err == segment.ErrClosed { + // index has been closed + _ = ourSnapshot.DecRef() + + // continue the workloop on a user triggered cancel + if ctrlMsg.doneCh != nil { + close(ctrlMsg.doneCh) + ctrlMsg = nil + continue OUTER + } + + // exit the workloop on index closure + ctrlMsg = nil + break OUTER + } + s.fireAsyncError(fmt.Errorf("merging err: %v", err)) + _ = ourSnapshot.DecRef() + atomic.AddUint64(&s.stats.TotFileMergeLoopErr, 1) + continue OUTER + } + + if ctrlMsg.doneCh != nil { + close(ctrlMsg.doneCh) + } + ctrlMsg = nil + + lastEpochMergePlanned = ourSnapshot.epoch + + atomic.StoreUint64(&s.stats.LastMergedEpoch, ourSnapshot.epoch) + + s.fireEvent(EventKindMergerProgress, time.Since(startTime)) + } + _ = ourSnapshot.DecRef() + + // tell the persister we're waiting for changes + // first make a epochWatcher chan + ew := &epochWatcher{ + epoch: lastEpochMergePlanned, + notifyCh: make(notificationChan, 1), + } + + // give it to the persister + select { + case <-s.closeCh: + break OUTER + case s.persisterNotifier <- ew: + case ctrlMsg = <-s.forceMergeRequestCh: + continue OUTER + } + + // now wait for persister (but also detect close) + select { + case <-s.closeCh: + break OUTER + case <-ew.notifyCh: + case ctrlMsg = <-s.forceMergeRequestCh: + } + } + + atomic.AddUint64(&s.stats.TotFileMergeLoopEnd, 1) + } +} + +type mergerCtrl struct { + ctx context.Context + options *mergeplan.MergePlanOptions + doneCh chan struct{} +} + +// ForceMerge helps users trigger a merge operation on +// an online scorch index. +func (s *Scorch) ForceMerge(ctx context.Context, + mo *mergeplan.MergePlanOptions) error { + // check whether force merge is already under processing + s.rootLock.Lock() + if s.stats.TotFileMergeForceOpsStarted > + s.stats.TotFileMergeForceOpsCompleted { + s.rootLock.Unlock() + return fmt.Errorf("force merge already in progress") + } + + s.stats.TotFileMergeForceOpsStarted++ + s.rootLock.Unlock() + + if mo != nil { + err := mergeplan.ValidateMergePlannerOptions(mo) + if err != nil { + return err + } + } else { + // assume the default single segment merge policy + mo = &mergeplan.SingleSegmentMergePlanOptions + } + msg := &mergerCtrl{options: mo, + doneCh: make(chan struct{}), + ctx: ctx, + } + + // request the merger perform a force merge + select { + case s.forceMergeRequestCh <- msg: + case <-s.closeCh: + return nil + } + + // wait for the force merge operation completion + select { + case <-msg.doneCh: + atomic.AddUint64(&s.stats.TotFileMergeForceOpsCompleted, 1) + case <-s.closeCh: + } + + return nil +} + +func (s *Scorch) parseMergePlannerOptions() (*mergeplan.MergePlanOptions, + error) { + mergePlannerOptions := mergeplan.DefaultMergePlanOptions + + po, err := s.parsePersisterOptions() + if err != nil { + return nil, err + } + // by default use the MaxSizeInMemoryMergePerWorker from the persister option + // as the FloorSegmentFileSize for the merge planner which would be the + // first tier size in the planning. If the value is 0, then we don't use the + // file size in the planning. + mergePlannerOptions.FloorSegmentFileSize = int64(po.MaxSizeInMemoryMergePerWorker) + + if v, ok := s.config["scorchMergePlanOptions"]; ok { + b, err := util.MarshalJSON(v) + if err != nil { + return &mergePlannerOptions, err + } + + err = util.UnmarshalJSON(b, &mergePlannerOptions) + if err != nil { + return &mergePlannerOptions, err + } + + err = mergeplan.ValidateMergePlannerOptions(&mergePlannerOptions) + if err != nil { + return nil, err + } + } + return &mergePlannerOptions, nil +} + +type closeChWrapper struct { + ch1 chan struct{} + ctx context.Context + closeCh chan struct{} + cancelCh chan struct{} +} + +func newCloseChWrapper(ch1 chan struct{}, + ctx context.Context) *closeChWrapper { + return &closeChWrapper{ + ch1: ch1, + ctx: ctx, + closeCh: make(chan struct{}), + cancelCh: make(chan struct{}), + } +} + +func (w *closeChWrapper) close() { + close(w.closeCh) +} + +func (w *closeChWrapper) listen() { + select { + case <-w.ch1: + close(w.cancelCh) + case <-w.ctx.Done(): + close(w.cancelCh) + case <-w.closeCh: + } +} + +func (s *Scorch) planMergeAtSnapshot(ctx context.Context, + options *mergeplan.MergePlanOptions, ourSnapshot *IndexSnapshot) error { + // build list of persisted segments in this snapshot + var onlyPersistedSnapshots []mergeplan.Segment + for _, segmentSnapshot := range ourSnapshot.segment { + if _, ok := segmentSnapshot.segment.(segment.PersistedSegment); ok { + onlyPersistedSnapshots = append(onlyPersistedSnapshots, segmentSnapshot) + } + } + + atomic.AddUint64(&s.stats.TotFileMergePlan, 1) + + // give this list to the planner + resultMergePlan, err := mergeplan.Plan(onlyPersistedSnapshots, options) + if err != nil { + atomic.AddUint64(&s.stats.TotFileMergePlanErr, 1) + return fmt.Errorf("merge planning err: %v", err) + } + if resultMergePlan == nil { + // nothing to do + atomic.AddUint64(&s.stats.TotFileMergePlanNone, 1) + return nil + } + atomic.AddUint64(&s.stats.TotFileMergePlanOk, 1) + + atomic.AddUint64(&s.stats.TotFileMergePlanTasks, uint64(len(resultMergePlan.Tasks))) + + // process tasks in serial for now + var filenames []string + + cw := newCloseChWrapper(s.closeCh, ctx) + defer cw.close() + + go cw.listen() + + for _, task := range resultMergePlan.Tasks { + if len(task.Segments) == 0 { + atomic.AddUint64(&s.stats.TotFileMergePlanTasksSegmentsEmpty, 1) + continue + } + + atomic.AddUint64(&s.stats.TotFileMergePlanTasksSegments, uint64(len(task.Segments))) + + oldMap := make(map[uint64]*SegmentSnapshot, len(task.Segments)) + newSegmentID := atomic.AddUint64(&s.nextSegmentID, 1) + segmentsToMerge := make([]segment.Segment, 0, len(task.Segments)) + docsToDrop := make([]*roaring.Bitmap, 0, len(task.Segments)) + mergedSegHistory := make(map[uint64]*mergedSegmentHistory, len(task.Segments)) + + for _, planSegment := range task.Segments { + if segSnapshot, ok := planSegment.(*SegmentSnapshot); ok { + oldMap[segSnapshot.id] = segSnapshot + mergedSegHistory[segSnapshot.id] = &mergedSegmentHistory{ + workerID: 0, + oldSegment: segSnapshot, + } + if persistedSeg, ok := segSnapshot.segment.(segment.PersistedSegment); ok { + if segSnapshot.LiveSize() == 0 { + atomic.AddUint64(&s.stats.TotFileMergeSegmentsEmpty, 1) + oldMap[segSnapshot.id] = nil + delete(mergedSegHistory, segSnapshot.id) + } else { + segmentsToMerge = append(segmentsToMerge, segSnapshot.segment) + docsToDrop = append(docsToDrop, segSnapshot.deleted) + } + // track the files getting merged for unsetting the + // removal ineligibility. This helps to unflip files + // even with fast merger, slow persister work flows. + path := persistedSeg.Path() + filenames = append(filenames, + strings.TrimPrefix(path, s.path+string(os.PathSeparator))) + } + } + } + + var seg segment.Segment + var filename string + if len(segmentsToMerge) > 0 { + filename = zapFileName(newSegmentID) + s.markIneligibleForRemoval(filename) + path := s.path + string(os.PathSeparator) + filename + + fileMergeZapStartTime := time.Now() + + atomic.AddUint64(&s.stats.TotFileMergeZapBeg, 1) + prevBytesReadTotal := cumulateBytesRead(segmentsToMerge) + newDocNums, _, err := s.segPlugin.Merge(segmentsToMerge, docsToDrop, path, + cw.cancelCh, s) + atomic.AddUint64(&s.stats.TotFileMergeZapEnd, 1) + + fileMergeZapTime := uint64(time.Since(fileMergeZapStartTime)) + atomic.AddUint64(&s.stats.TotFileMergeZapTime, fileMergeZapTime) + if atomic.LoadUint64(&s.stats.MaxFileMergeZapTime) < fileMergeZapTime { + atomic.StoreUint64(&s.stats.MaxFileMergeZapTime, fileMergeZapTime) + } + + if err != nil { + s.unmarkIneligibleForRemoval(filename) + atomic.AddUint64(&s.stats.TotFileMergePlanTasksErr, 1) + if err == segment.ErrClosed { + return err + } + return fmt.Errorf("merging failed: %v", err) + } + + seg, err = s.segPlugin.Open(path) + if err != nil { + s.unmarkIneligibleForRemoval(filename) + atomic.AddUint64(&s.stats.TotFileMergePlanTasksErr, 1) + return err + } + + totalBytesRead := seg.BytesRead() + prevBytesReadTotal + seg.ResetBytesRead(totalBytesRead) + + for i, segNewDocNums := range newDocNums { + if mergedSegHistory[task.Segments[i].Id()] != nil { + mergedSegHistory[task.Segments[i].Id()].oldNewDocIDs = segNewDocNums + } + } + + atomic.AddUint64(&s.stats.TotFileMergeSegments, uint64(len(segmentsToMerge))) + } + + sm := &segmentMerge{ + id: []uint64{newSegmentID}, + mergedSegHistory: mergedSegHistory, + new: []segment.Segment{seg}, + newCount: seg.Count(), + notifyCh: make(chan *mergeTaskIntroStatus), + mmaped: 1, + } + + s.fireEvent(EventKindMergeTaskIntroductionStart, 0) + + // give it to the introducer + select { + case <-s.closeCh: + _ = seg.Close() + return segment.ErrClosed + case s.merges <- sm: + atomic.AddUint64(&s.stats.TotFileMergeIntroductions, 1) + } + + introStartTime := time.Now() + // it is safe to blockingly wait for the merge introduction + // here as the introducer is bound to handle the notify channel. + introStatus := <-sm.notifyCh + introTime := uint64(time.Since(introStartTime)) + atomic.AddUint64(&s.stats.TotFileMergeZapIntroductionTime, introTime) + if atomic.LoadUint64(&s.stats.MaxFileMergeZapIntroductionTime) < introTime { + atomic.StoreUint64(&s.stats.MaxFileMergeZapIntroductionTime, introTime) + } + atomic.AddUint64(&s.stats.TotFileMergeIntroductionsDone, 1) + if introStatus != nil && introStatus.indexSnapshot != nil { + _ = introStatus.indexSnapshot.DecRef() + if introStatus.skipped { + // close the segment on skipping introduction. + s.unmarkIneligibleForRemoval(filename) + _ = seg.Close() + } + } + + atomic.AddUint64(&s.stats.TotFileMergePlanTasksDone, 1) + + s.fireEvent(EventKindMergeTaskIntroduction, 0) + } + + // once all the newly merged segment introductions are done, + // its safe to unflip the removal ineligibility for the replaced + // older segments + for _, f := range filenames { + s.unmarkIneligibleForRemoval(f) + } + + return nil +} + +type mergeTaskIntroStatus struct { + indexSnapshot *IndexSnapshot + skipped bool +} + +// this is important when it comes to introducing multiple merged segments in a +// single introducer channel push. That way there is a check to ensure that the +// file count doesn't explode during the index's lifetime. +type mergedSegmentHistory struct { + workerID uint64 + oldNewDocIDs []uint64 + oldSegment *SegmentSnapshot +} + +type segmentMerge struct { + id []uint64 + new []segment.Segment + mergedSegHistory map[uint64]*mergedSegmentHistory + notifyCh chan *mergeTaskIntroStatus + mmaped uint32 + newCount uint64 +} + +func cumulateBytesRead(sbs []segment.Segment) uint64 { + var rv uint64 + for _, seg := range sbs { + rv += seg.BytesRead() + } + return rv +} + +func closeNewMergedSegments(segs []segment.Segment) error { + for _, seg := range segs { + if seg != nil { + _ = seg.DecRef() + } + } + return nil +} + +// mergeAndPersistInMemorySegments takes an IndexSnapshot and a list of in-memory segments, +// which are merged and persisted to disk concurrently. These are then introduced as +// the new root snapshot in one-shot. +func (s *Scorch) mergeAndPersistInMemorySegments(snapshot *IndexSnapshot, + flushableObjs []*flushable) (*IndexSnapshot, []uint64, error) { + atomic.AddUint64(&s.stats.TotMemMergeBeg, 1) + + memMergeZapStartTime := time.Now() + + atomic.AddUint64(&s.stats.TotMemMergeZapBeg, 1) + + var wg sync.WaitGroup + // we're tracking the merged segments and their doc number per worker + // to be able to introduce them all at once, so the first dimension of the + // slices here correspond to workerID + newDocIDsSet := make([][][]uint64, len(flushableObjs)) + newMergedSegments := make([]segment.Segment, len(flushableObjs)) + newMergedSegmentIDs := make([]uint64, len(flushableObjs)) + numFlushes := len(flushableObjs) + var numSegments, newMergedCount uint64 + var em sync.Mutex + var errs []error + + // deploy the workers to merge and flush the batches of segments concurrently + // and create a new file segment + for i := 0; i < numFlushes; i++ { + wg.Add(1) + go func(segsBatch []segment.Segment, dropsBatch []*roaring.Bitmap, id int) { + defer wg.Done() + newSegmentID := atomic.AddUint64(&s.nextSegmentID, 1) + filename := zapFileName(newSegmentID) + path := s.path + string(os.PathSeparator) + filename + + // the newly merged segment is already flushed out to disk, just needs + // to be opened using mmap. + newDocIDs, _, err := + s.segPlugin.Merge(segsBatch, dropsBatch, path, s.closeCh, s) + if err != nil { + em.Lock() + errs = append(errs, err) + em.Unlock() + atomic.AddUint64(&s.stats.TotMemMergeErr, 1) + return + } + // to prevent accidental cleanup of this newly created file, mark it + // as ineligible for removal. this will be flipped back when the bolt + // is updated - which is valid, since the snapshot updated in bolt is + // cleaned up only if its zero ref'd (MB-66163 for more details) + s.markIneligibleForRemoval(filename) + newMergedSegmentIDs[id] = newSegmentID + newDocIDsSet[id] = newDocIDs + newMergedSegments[id], err = s.segPlugin.Open(path) + if err != nil { + em.Lock() + errs = append(errs, err) + em.Unlock() + atomic.AddUint64(&s.stats.TotMemMergeErr, 1) + return + } + atomic.AddUint64(&newMergedCount, newMergedSegments[id].Count()) + atomic.AddUint64(&numSegments, uint64(len(segsBatch))) + }(flushableObjs[i].segments, flushableObjs[i].drops, i) + } + wg.Wait() + + if errs != nil { + // close the new merged segments + _ = closeNewMergedSegments(newMergedSegments) + var errf error + for _, err := range errs { + if err == segment.ErrClosed { + // the index snapshot was closed which will be handled gracefully + // by retrying the whole merge+flush operation in a later iteration + // so its safe to early exit the same error. + return nil, nil, err + } + errf = fmt.Errorf("%w; %v", errf, err) + } + return nil, nil, errf + } + + atomic.AddUint64(&s.stats.TotMemMergeZapEnd, 1) + + memMergeZapTime := uint64(time.Since(memMergeZapStartTime)) + atomic.AddUint64(&s.stats.TotMemMergeZapTime, memMergeZapTime) + if atomic.LoadUint64(&s.stats.MaxMemMergeZapTime) < memMergeZapTime { + atomic.StoreUint64(&s.stats.MaxMemMergeZapTime, memMergeZapTime) + } + + // update the segmentMerge task with the newly merged + flushed segments which + // are to be introduced atomically. + sm := &segmentMerge{ + id: newMergedSegmentIDs, + new: newMergedSegments, + mergedSegHistory: make(map[uint64]*mergedSegmentHistory, numSegments), + notifyCh: make(chan *mergeTaskIntroStatus), + newCount: newMergedCount, + } + + // create a history map which maps the old in-memory segments with the specific + // persister worker (also the specific file segment its going to be part of) + // which flushed it out. This map will be used on the introducer side to out-ref + // the in-memory segments and also track the new tombstones if present. + for i, flushable := range flushableObjs { + for j, idx := range flushable.sbIdxs { + ss := snapshot.segment[idx] + // oldSegmentSnapshot.id -> {workerID, oldSegmentSnapshot, docIDs} + sm.mergedSegHistory[ss.id] = &mergedSegmentHistory{ + workerID: uint64(i), + oldNewDocIDs: newDocIDsSet[i][j], + oldSegment: ss, + } + } + } + + select { // send to introducer + case <-s.closeCh: + _ = closeNewMergedSegments(newMergedSegments) + return nil, nil, segment.ErrClosed + case s.merges <- sm: + } + + // blockingly wait for the introduction to complete + var newSnapshot *IndexSnapshot + introStatus := <-sm.notifyCh + if introStatus != nil && introStatus.indexSnapshot != nil { + newSnapshot = introStatus.indexSnapshot + atomic.AddUint64(&s.stats.TotMemMergeSegments, uint64(numSegments)) + atomic.AddUint64(&s.stats.TotMemMergeDone, 1) + if introStatus.skipped { + // close the segment on skipping introduction. + _ = newSnapshot.DecRef() + _ = closeNewMergedSegments(newMergedSegments) + newSnapshot = nil + } + } + + return newSnapshot, newMergedSegmentIDs, nil +} + +func (s *Scorch) ReportBytesWritten(bytesWritten uint64) { + atomic.AddUint64(&s.stats.TotFileMergeWrittenBytes, bytesWritten) +} diff --git a/index/scorch/merge_test.go b/index/scorch/merge_test.go new file mode 100644 index 0000000..b324cb7 --- /dev/null +++ b/index/scorch/merge_test.go @@ -0,0 +1,157 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "sync" + "sync/atomic" + "testing" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +func TestObsoleteSegmentMergeIntroduction(t *testing.T) { + testConfig := CreateConfig("TestObsoleteSegmentMergeIntroduction") + err := InitTest(testConfig) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(testConfig) + if err != nil { + t.Fatal(err) + } + }() + + var introComplete, mergeIntroStart, mergeIntroComplete sync.WaitGroup + introComplete.Add(1) + mergeIntroStart.Add(1) + mergeIntroComplete.Add(1) + var segIntroCompleted int + RegistryEventCallbacks["test"] = func(e Event) bool { + switch e.Kind { + case EventKindBatchIntroduction: + segIntroCompleted++ + if segIntroCompleted == 3 { + // all 3 segments introduced + introComplete.Done() + } + case EventKindMergeTaskIntroductionStart: + // signal the start of merge task introduction so that + // we can introduce a new batch which obsoletes the + // merged segment's contents. + mergeIntroStart.Done() + // hold the merge task introduction until the merged segment contents + // are obsoleted with the next batch/segment introduction. + introComplete.Wait() + case EventKindMergeTaskIntroduction: + // signal the completion of the merge task introduction. + mergeIntroComplete.Done() + + } + + return true + } + + ourConfig := make(map[string]interface{}, len(testConfig)) + for k, v := range testConfig { + ourConfig[k] = v + } + ourConfig["eventCallbackName"] = "test" + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, ourConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // first introduce two documents over two batches. + batch := index.NewBatch() + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test3"))) + batch.Update(doc) + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + + batch.Reset() + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2updated"))) + batch.Update(doc) + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + + // wait until the merger trying to introduce the new merged segment. + mergeIntroStart.Wait() + + // execute another batch which obsoletes the contents of the new merged + // segment awaiting introduction. + batch.Reset() + batch.Delete("1") + batch.Delete("2") + doc = document.NewDocument("3") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test3updated"))) + batch.Update(doc) + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + + // wait until the merge task introduction complete. + mergeIntroComplete.Wait() + + idxr, err := idx.Reader() + if err != nil { + t.Error(err) + } + + numSegments := len(idxr.(*IndexSnapshot).segment) + if numSegments != 1 { + t.Errorf("expected one segment at the root, got: %d", numSegments) + } + + skipIntroCount := atomic.LoadUint64(&idxr.(*IndexSnapshot).parent.stats.TotFileMergeIntroductionsObsoleted) + if skipIntroCount != 1 { + t.Errorf("expected one obsolete merge segment skipping the introduction, got: %d", skipIntroCount) + } + + docCount, err := idxr.DocCount() + if err != nil { + t.Fatal(err) + } + if docCount != 1 { + t.Errorf("Expected document count to be %d got %d", 1, docCount) + } + + err = idxr.Close() + if err != nil { + t.Fatal(err) + } +} diff --git a/index/scorch/mergeplan/merge_plan.go b/index/scorch/mergeplan/merge_plan.go new file mode 100644 index 0000000..8ddde74 --- /dev/null +++ b/index/scorch/mergeplan/merge_plan.go @@ -0,0 +1,454 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package mergeplan provides a segment merge planning approach that's +// inspired by Lucene's TieredMergePolicy.java and descriptions like +// http://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html +package mergeplan + +import ( + "errors" + "fmt" + "math" + "sort" + "strings" +) + +// A Segment represents the information that the planner needs to +// calculate segment merging. +type Segment interface { + // Unique id of the segment -- used for sorting. + Id() uint64 + + // Full segment size (the size before any logical deletions). + FullSize() int64 + + // Size of the live data of the segment; i.e., FullSize() minus + // any logical deletions. + LiveSize() int64 + + HasVector() bool + + // Size of the persisted segment file. + FileSize() int64 +} + +// Plan() will functionally compute a merge plan. A segment will be +// assigned to at most a single MergeTask in the output MergePlan. A +// segment not assigned to any MergeTask means the segment should +// remain unmerged. +func Plan(segments []Segment, o *MergePlanOptions) (*MergePlan, error) { + return plan(segments, o) +} + +// A MergePlan is the result of the Plan() API. +// +// The planner doesn’t know how or whether these tasks are executed -- +// that’s up to a separate merge execution system, which might execute +// these tasks concurrently or not, and which might execute all the +// tasks or not. +type MergePlan struct { + Tasks []*MergeTask +} + +// A MergeTask represents several segments that should be merged +// together into a single segment. +type MergeTask struct { + Segments []Segment +} + +// The MergePlanOptions is designed to be reusable between planning calls. +type MergePlanOptions struct { + // Max # segments per logarithmic tier, or max width of any + // logarithmic “step”. Smaller values mean more merging but fewer + // segments. Should be >= SegmentsPerMergeTask, else you'll have + // too much merging. + MaxSegmentsPerTier int + + // Max size of any segment produced after merging. Actual + // merging, however, may produce segment sizes different than the + // planner’s predicted sizes. + MaxSegmentSize int64 + + // Max size (in bytes) of the persisted segment file that contains the + // vectors. This is used to prevent merging of segments that + // contain vectors that are too large. + MaxSegmentFileSize int64 + + // The growth factor for each tier in a staircase of idealized + // segments computed by CalcBudget(). + TierGrowth float64 + + // The number of segments in any resulting MergeTask. e.g., + // len(result.Tasks[ * ].Segments) == SegmentsPerMergeTask. + SegmentsPerMergeTask int + + // Small segments are rounded up to this size, i.e., treated as + // equal (floor) size for consideration. This is to prevent lots + // of tiny segments from resulting in a long tail in the index. + FloorSegmentSize int64 + + // Small segments' file size are rounded up to this size to prevent lot + // of tiny segments causing a long tail in the index. + FloorSegmentFileSize int64 + + // Controls how aggressively merges that reclaim more deletions + // are favored. Higher values will more aggressively target + // merges that reclaim deletions, but be careful not to go so high + // that way too much merging takes place; a value of 3.0 is + // probably nearly too high. A value of 0.0 means deletions don't + // impact merge selection. + ReclaimDeletesWeight float64 + + // Optional, defaults to mergeplan.CalcBudget(). + CalcBudget func(totalSize int64, firstTierSize int64, + o *MergePlanOptions) (budgetNumSegments int) + + // Optional, defaults to mergeplan.ScoreSegments(). + ScoreSegments func(segments []Segment, o *MergePlanOptions) float64 + + // Optional. + Logger func(string) +} + +// Returns the higher of the input or FloorSegmentSize. +func (o *MergePlanOptions) RaiseToFloorSegmentSize(s int64) int64 { + if s > o.FloorSegmentSize { + return s + } + return o.FloorSegmentSize +} + +func (o *MergePlanOptions) RaiseToFloorSegmentFileSize(s int64) int64 { + if s > o.FloorSegmentFileSize { + return s + } + return o.FloorSegmentFileSize +} + +// MaxSegmentSizeLimit represents the maximum size of a segment, +// this limit comes with hit-1 optimisation/max encoding limit uint31. +const MaxSegmentSizeLimit = 1<<31 - 1 + +// ErrMaxSegmentSizeTooLarge is returned when the size of the segment +// exceeds the MaxSegmentSizeLimit +var ErrMaxSegmentSizeTooLarge = errors.New("MaxSegmentSize exceeds the size limit") + +// DefaultMergePlanOptions suggests the default options. +var DefaultMergePlanOptions = MergePlanOptions{ + MaxSegmentsPerTier: 10, + MaxSegmentSize: 5000000, + MaxSegmentFileSize: 4000000000, // 4GB + TierGrowth: 10.0, + SegmentsPerMergeTask: 10, + FloorSegmentSize: 2000, + ReclaimDeletesWeight: 2.0, +} + +// SingleSegmentMergePlanOptions helps in creating a +// single segment index. +var SingleSegmentMergePlanOptions = MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 1 << 30, + MaxSegmentFileSize: 1 << 40, + TierGrowth: 1.0, + SegmentsPerMergeTask: 10, + FloorSegmentSize: 1 << 30, + ReclaimDeletesWeight: 2.0, + FloorSegmentFileSize: 1 << 40, +} + +// ------------------------------------------- + +func plan(segmentsIn []Segment, o *MergePlanOptions) (*MergePlan, error) { + if len(segmentsIn) <= 1 { + return nil, nil + } + + if o == nil { + o = &DefaultMergePlanOptions + } + + segments := append([]Segment(nil), segmentsIn...) // Copy. + + sort.Sort(byLiveSizeDescending(segments)) + + var minLiveSize int64 = math.MaxInt64 + + var eligibles []Segment + var eligiblesLiveSize int64 + var eligiblesFileSize int64 + var minFileSize int64 = math.MaxInt64 + + for _, segment := range segments { + if minLiveSize > segment.LiveSize() { + minLiveSize = segment.LiveSize() + } + + if minFileSize > segment.FileSize() { + minFileSize = segment.FileSize() + } + + isEligible := segment.LiveSize() < o.MaxSegmentSize/2 + // An eligible segment (based on #documents) may be too large + // and thus need a stricter check based on the file size. + // This is particularly important for segments that contain + // vectors. + if isEligible && segment.HasVector() && o.MaxSegmentFileSize > 0 { + isEligible = segment.FileSize() < o.MaxSegmentFileSize/2 + } + + // Only small-enough segments are eligible. + if isEligible { + eligibles = append(eligibles, segment) + eligiblesLiveSize += segment.LiveSize() + eligiblesFileSize += segment.FileSize() + } + } + + calcBudget := o.CalcBudget + if calcBudget == nil { + calcBudget = CalcBudget + } + + var budgetNumSegments int + if o.FloorSegmentFileSize > 0 { + minFileSize = o.RaiseToFloorSegmentFileSize(minFileSize) + budgetNumSegments = calcBudget(eligiblesFileSize, minFileSize, o) + + } else { + minLiveSize = o.RaiseToFloorSegmentSize(minLiveSize) + budgetNumSegments = calcBudget(eligiblesLiveSize, minLiveSize, o) + } + + scoreSegments := o.ScoreSegments + if scoreSegments == nil { + scoreSegments = ScoreSegments + } + + rv := &MergePlan{} + + var empties []Segment + for _, eligible := range eligibles { + if eligible.LiveSize() <= 0 { + empties = append(empties, eligible) + } + } + if len(empties) > 0 { + rv.Tasks = append(rv.Tasks, &MergeTask{Segments: empties}) + eligibles = removeSegments(eligibles, empties) + } + + // While we’re over budget, keep looping, which might produce + // another MergeTask. + for len(eligibles) > 0 && (len(eligibles)+len(rv.Tasks)) > budgetNumSegments { + // Track a current best roster as we examine and score + // potential rosters of merges. + var bestRoster []Segment + var bestRosterScore float64 // Lower score is better. + + for startIdx := 0; startIdx < len(eligibles); startIdx++ { + var roster []Segment + var rosterLiveSize int64 + var rosterFileSize int64 // useful for segments with vectors + + for idx := startIdx; idx < len(eligibles) && len(roster) < o.SegmentsPerMergeTask; idx++ { + eligible := eligibles[idx] + + if rosterLiveSize+eligible.LiveSize() >= o.MaxSegmentSize { + continue + } + + if eligible.HasVector() { + efs := eligible.FileSize() + if rosterFileSize+efs >= o.MaxSegmentFileSize { + continue + } + rosterFileSize += efs + } + + roster = append(roster, eligible) + rosterLiveSize += eligible.LiveSize() + } + + if len(roster) > 0 { + rosterScore := scoreSegments(roster, o) + + if len(bestRoster) == 0 || rosterScore < bestRosterScore { + bestRoster = roster + bestRosterScore = rosterScore + } + } + } + + if len(bestRoster) == 0 { + return rv, nil + } + + rv.Tasks = append(rv.Tasks, &MergeTask{Segments: bestRoster}) + + eligibles = removeSegments(eligibles, bestRoster) + } + + return rv, nil +} + +// Compute the number of segments that would be needed to cover the +// totalSize, by climbing up a logarithmically growing staircase of +// segment tiers. +func CalcBudget(totalSize int64, firstTierSize int64, o *MergePlanOptions) ( + budgetNumSegments int) { + tierSize := firstTierSize + if tierSize < 1 { + tierSize = 1 + } + + maxSegmentsPerTier := o.MaxSegmentsPerTier + if maxSegmentsPerTier < 1 { + maxSegmentsPerTier = 1 + } + + tierGrowth := o.TierGrowth + if tierGrowth < 1.0 { + tierGrowth = 1.0 + } + + for totalSize > 0 { + segmentsInTier := float64(totalSize) / float64(tierSize) + if segmentsInTier < float64(maxSegmentsPerTier) { + budgetNumSegments += int(math.Ceil(segmentsInTier)) + break + } + + budgetNumSegments += maxSegmentsPerTier + totalSize -= int64(maxSegmentsPerTier) * tierSize + tierSize = int64(float64(tierSize) * tierGrowth) + } + + return budgetNumSegments +} + +// Of note, removeSegments() keeps the ordering of the results stable. +func removeSegments(segments []Segment, toRemove []Segment) []Segment { + rv := make([]Segment, 0, len(segments)-len(toRemove)) +OUTER: + for _, segment := range segments { + for _, r := range toRemove { + if segment == r { + continue OUTER + } + } + rv = append(rv, segment) + } + return rv +} + +// Smaller result score is better. +func ScoreSegments(segments []Segment, o *MergePlanOptions) float64 { + var totBeforeSize int64 + var totAfterSize int64 + var totAfterSizeFloored int64 + + for _, segment := range segments { + totBeforeSize += segment.FullSize() + totAfterSize += segment.LiveSize() + totAfterSizeFloored += o.RaiseToFloorSegmentSize(segment.LiveSize()) + } + + if totBeforeSize <= 0 || totAfterSize <= 0 || totAfterSizeFloored <= 0 { + return 0 + } + + // Roughly guess the "balance" of the segments -- whether the + // segments are about the same size. + balance := + float64(o.RaiseToFloorSegmentSize(segments[0].LiveSize())) / + float64(totAfterSizeFloored) + + // Gently favor smaller merges over bigger ones. We don't want to + // make the exponent too large else we end up with poor merges of + // small segments in order to avoid the large merges. + score := balance * math.Pow(float64(totAfterSize), 0.05) + + // Strongly favor merges that reclaim deletes. + nonDelRatio := float64(totAfterSize) / float64(totBeforeSize) + + score *= math.Pow(nonDelRatio, o.ReclaimDeletesWeight) + + return score +} + +// ------------------------------------------ + +// ToBarChart returns an ASCII rendering of the segments and the plan. +// The barMax is the max width of the bars in the bar chart. +func ToBarChart(prefix string, barMax int, segments []Segment, plan *MergePlan) string { + rv := make([]string, 0, len(segments)) + + var maxFullSize int64 + for _, segment := range segments { + if maxFullSize < segment.FullSize() { + maxFullSize = segment.FullSize() + } + } + if maxFullSize < 0 { + maxFullSize = 1 + } + + for _, segment := range segments { + barFull := int(segment.FullSize()) + barLive := int(segment.LiveSize()) + + if maxFullSize > int64(barMax) { + barFull = int(float64(barMax) * float64(barFull) / float64(maxFullSize)) + barLive = int(float64(barMax) * float64(barLive) / float64(maxFullSize)) + } + + barKind := " " + barChar := "." + + if plan != nil { + TASK_LOOP: + for taski, task := range plan.Tasks { + for _, taskSegment := range task.Segments { + if taskSegment == segment { + barKind = "*" + barChar = fmt.Sprintf("%d", taski) + break TASK_LOOP + } + } + } + } + + bar := + strings.Repeat(barChar, barLive)[0:barLive] + + strings.Repeat("x", barFull-barLive)[0:barFull-barLive] + + rv = append(rv, fmt.Sprintf("%s %5d: %5d /%5d - %s %s", prefix, + segment.Id(), + segment.LiveSize(), + segment.FullSize(), + barKind, bar)) + } + + return strings.Join(rv, "\n") +} + +// ValidateMergePlannerOptions validates the merge planner options +func ValidateMergePlannerOptions(options *MergePlanOptions) error { + if options.MaxSegmentSize > MaxSegmentSizeLimit { + return ErrMaxSegmentSizeTooLarge + } + return nil +} diff --git a/index/scorch/mergeplan/merge_plan_test.go b/index/scorch/mergeplan/merge_plan_test.go new file mode 100644 index 0000000..d078d7e --- /dev/null +++ b/index/scorch/mergeplan/merge_plan_test.go @@ -0,0 +1,721 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mergeplan + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "reflect" + "sort" + "testing" + "time" +) + +// Implements the Segment interface for testing, +type segment struct { + MyId uint64 + MyFullSize int64 + MyLiveSize int64 + + MyHasVector bool + MyFileSize int64 +} + +func (s *segment) Id() uint64 { return s.MyId } +func (s *segment) FullSize() int64 { return s.MyFullSize } +func (s *segment) LiveSize() int64 { return s.MyLiveSize } +func (s *segment) HasVector() bool { return s.MyHasVector } +func (s *segment) FileSize() int64 { return s.MyFileSize } + +func makeLinearSegments(n int) (rv []Segment) { + for i := 0; i < n; i++ { + rv = append(rv, &segment{ + MyId: uint64(i), + MyFullSize: int64(i), + MyLiveSize: int64(i), + }) + } + return rv +} + +// ---------------------------------------- + +func TestSimplePlan(t *testing.T) { + segs := makeLinearSegments(10) + + tests := []struct { + Desc string + Segments []Segment + Options *MergePlanOptions + ExpectPlan *MergePlan + ExpectErr error + }{ + { + "nil segments", + nil, nil, nil, nil, + }, + { + "empty segments", + []Segment{}, + nil, nil, nil, + }, + { + "1 segment", + []Segment{segs[1]}, + nil, + nil, + nil, + }, + { + "2 segments", + []Segment{ + segs[1], + segs[2], + }, + nil, + &MergePlan{ + Tasks: []*MergeTask{ + { + Segments: []Segment{ + segs[2], + segs[1], + }, + }, + }, + }, + nil, + }, + { + "3 segments", + []Segment{ + segs[1], + segs[2], + segs[9], + }, + nil, + &MergePlan{ + Tasks: []*MergeTask{ + { + Segments: []Segment{ + segs[9], + segs[2], + segs[1], + }, + }, + }, + }, + nil, + }, + { + "many segments", + []Segment{ + segs[1], + segs[2], + segs[3], + segs[4], + segs[5], + segs[6], + }, + &MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 1000, + TierGrowth: 2.0, + SegmentsPerMergeTask: 2, + FloorSegmentSize: 1, + }, + &MergePlan{ + Tasks: []*MergeTask{ + { + Segments: []Segment{ + segs[6], + segs[5], + }, + }, + }, + }, + nil, + }, + } + + for testi, test := range tests { + plan, err := Plan(test.Segments, test.Options) + + if err != test.ExpectErr { + testj, _ := json.Marshal(&test) + + t.Errorf("testi: %d, test: %s, got err: %v", testi, testj, err) + } + + if !reflect.DeepEqual(plan, test.ExpectPlan) { + testj, _ := json.Marshal(&test) + + planj, _ := json.Marshal(&plan) + + t.Errorf("testi: %d, test: %s, got plan: %s", + testi, testj, planj) + } + } +} + +// ---------------------------------------- + +func TestSort(t *testing.T) { + segs := makeLinearSegments(10) + + sort.Sort(byLiveSizeDescending(segs)) + + for i := 1; i < len(segs); i++ { + if segs[i].LiveSize() >= segs[i-1].LiveSize() { + t.Errorf("not descending") + } + } +} + +// ---------------------------------------- + +func TestCalcBudget(t *testing.T) { + tests := []struct { + totalSize int64 + firstTierSize int64 + o MergePlanOptions + expect int + }{ + {0, 0, MergePlanOptions{}, 0}, + {1, 0, MergePlanOptions{}, 1}, + {9, 0, MergePlanOptions{}, 9}, + { + 1, 1, + MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 1000, + TierGrowth: 2.0, + SegmentsPerMergeTask: 2, + FloorSegmentSize: 1, + }, + 1, + }, + { + 21, 1, + MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 1000, + TierGrowth: 2.0, + SegmentsPerMergeTask: 2, + FloorSegmentSize: 1, + }, + 5, + }, + { + 21, 1, + MergePlanOptions{ + MaxSegmentsPerTier: 2, + MaxSegmentSize: 1000, + TierGrowth: 2.0, + SegmentsPerMergeTask: 2, + FloorSegmentSize: 1, + }, + 7, + }, + { + 1000, 2000, DefaultMergePlanOptions, + 1, + }, + { + 5000, 2000, DefaultMergePlanOptions, + 3, + }, + { + 10000, 2000, DefaultMergePlanOptions, + 5, + }, + { + 30000, 2000, DefaultMergePlanOptions, + 11, + }, + { + 1000000, 2000, DefaultMergePlanOptions, + 24, + }, + { + 1000000000, 2000, DefaultMergePlanOptions, + 54, + }, + } + + for testi, test := range tests { + res := CalcBudget(test.totalSize, test.firstTierSize, &test.o) + if res != test.expect { + t.Errorf("testi: %d, test: %#v, res: %v", + testi, test, res) + } + } +} + +func TestCalcBudgetForSingleSegmentMergePolicy(t *testing.T) { + mpolicy := MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 1 << 30, // ~ 1 Billion + SegmentsPerMergeTask: 10, + FloorSegmentSize: 1 << 30, + } + + tests := []struct { + totalSize int64 + firstTierSize int64 + o MergePlanOptions + expect int + }{ + {0, mpolicy.RaiseToFloorSegmentSize(0), mpolicy, 0}, + {1, mpolicy.RaiseToFloorSegmentSize(1), mpolicy, 1}, + {9, mpolicy.RaiseToFloorSegmentSize(0), mpolicy, 1}, + {1, mpolicy.RaiseToFloorSegmentSize(1), mpolicy, 1}, + {21, mpolicy.RaiseToFloorSegmentSize(21), mpolicy, 1}, + {21, mpolicy.RaiseToFloorSegmentSize(21), mpolicy, 1}, + {1000, mpolicy.RaiseToFloorSegmentSize(2000), mpolicy, 1}, + {5000, mpolicy.RaiseToFloorSegmentSize(5000), mpolicy, 1}, + {10000, mpolicy.RaiseToFloorSegmentSize(10000), mpolicy, 1}, + {30000, mpolicy.RaiseToFloorSegmentSize(30000), mpolicy, 1}, + {1000000, mpolicy.RaiseToFloorSegmentSize(1000000), mpolicy, 1}, + {1000000000, 1 << 30, mpolicy, 1}, + {1013423541, 1 << 30, mpolicy, 1}, + {98765442, 1 << 30, mpolicy, 1}, + } + + for testi, test := range tests { + res := CalcBudget(test.totalSize, test.firstTierSize, &test.o) + if res != test.expect { + t.Errorf("testi: %d, test: %#v, res: %v", + testi, test, res) + } + } +} + +// ---------------------------------------- + +func TestInsert1SameSizedSegmentBetweenMerges(t *testing.T) { + o := &MergePlanOptions{ + MaxSegmentSize: 1000, + MaxSegmentsPerTier: 3, + TierGrowth: 3.0, + SegmentsPerMergeTask: 3, + } + + spec := testCyclesSpec{ + descrip: "i1sssbm", + verbose: os.Getenv("VERBOSE") == "i1sssbm" || os.Getenv("VERBOSE") == "y", + n: 200, + o: o, + beforePlan: func(spec *testCyclesSpec) { + spec.segments = append(spec.segments, &segment{ + MyId: spec.nextSegmentId, + MyFullSize: 1, + MyLiveSize: 1, + }) + spec.nextSegmentId++ + }, + } + + spec.runCycles(t) +} + +func TestInsertManySameSizedSegmentsBetweenMerges(t *testing.T) { + o := &MergePlanOptions{ + MaxSegmentSize: 1000, + MaxSegmentsPerTier: 3, + TierGrowth: 3.0, + SegmentsPerMergeTask: 3, + } + + spec := testCyclesSpec{ + descrip: "imsssbm", + verbose: os.Getenv("VERBOSE") == "imsssbm" || os.Getenv("VERBOSE") == "y", + n: 20, + o: o, + beforePlan: func(spec *testCyclesSpec) { + for i := 0; i < 10; i++ { + spec.segments = append(spec.segments, &segment{ + MyId: spec.nextSegmentId, + MyFullSize: 1, + MyLiveSize: 1, + }) + spec.nextSegmentId++ + } + }, + } + + spec.runCycles(t) +} + +func TestInsertManySameSizedSegmentsWithDeletionsBetweenMerges(t *testing.T) { + o := &MergePlanOptions{ + MaxSegmentSize: 1000, + MaxSegmentsPerTier: 3, + TierGrowth: 3.0, + SegmentsPerMergeTask: 3, + } + + spec := testCyclesSpec{ + descrip: "imssswdbm", + verbose: os.Getenv("VERBOSE") == "imssswdbm" || os.Getenv("VERBOSE") == "y", + n: 20, + o: o, + beforePlan: func(spec *testCyclesSpec) { + for i := 0; i < 10; i++ { + // Deletions are a shrinking of the live size. + for i, seg := range spec.segments { + if (spec.cycle+i)%5 == 0 { + s := seg.(*segment) + if s.MyLiveSize > 0 { + s.MyLiveSize -= 1 + } + } + } + + spec.segments = append(spec.segments, &segment{ + MyId: spec.nextSegmentId, + MyFullSize: 1, + MyLiveSize: 1, + }) + spec.nextSegmentId++ + } + }, + } + + spec.runCycles(t) +} + +func TestInsertManyDifferentSizedSegmentsBetweenMerges(t *testing.T) { + o := &MergePlanOptions{ + MaxSegmentSize: 1000, + MaxSegmentsPerTier: 3, + TierGrowth: 3.0, + SegmentsPerMergeTask: 3, + } + + spec := testCyclesSpec{ + descrip: "imdssbm", + verbose: os.Getenv("VERBOSE") == "imdssbm" || os.Getenv("VERBOSE") == "y", + n: 20, + o: o, + beforePlan: func(spec *testCyclesSpec) { + for i := 0; i < 10; i++ { + spec.segments = append(spec.segments, &segment{ + MyId: spec.nextSegmentId, + MyFullSize: int64(1 + (i % 5)), + MyLiveSize: int64(1 + (i % 5)), + }) + spec.nextSegmentId++ + } + }, + } + + spec.runCycles(t) +} + +func TestManySameSizedSegmentsWithDeletesBetweenMerges(t *testing.T) { + o := &MergePlanOptions{ + MaxSegmentSize: 1000, + MaxSegmentsPerTier: 3, + TierGrowth: 3.0, + SegmentsPerMergeTask: 3, + } + + var numPlansWithTasks int + + spec := testCyclesSpec{ + descrip: "mssswdbm", + verbose: os.Getenv("VERBOSE") == "mssswdbm" || os.Getenv("VERBOSE") == "y", + n: 20, + o: o, + beforePlan: func(spec *testCyclesSpec) { + // Deletions are a shrinking of the live size. + for i, seg := range spec.segments { + if (spec.cycle+i)%5 == 0 { + s := seg.(*segment) + if s.MyLiveSize > 0 { + s.MyLiveSize -= 1 + } + } + } + + for i := 0; i < 10; i++ { + spec.segments = append(spec.segments, &segment{ + MyId: spec.nextSegmentId, + MyFullSize: 1, + MyLiveSize: 1, + }) + spec.nextSegmentId++ + } + }, + afterPlan: func(spec *testCyclesSpec, plan *MergePlan) { + if plan != nil && len(plan.Tasks) > 0 { + numPlansWithTasks++ + } + }, + } + + spec.runCycles(t) + + if numPlansWithTasks <= 0 { + t.Errorf("expected some plans with tasks") + } +} + +func TestValidateMergePlannerOptions(t *testing.T) { + o := &MergePlanOptions{ + MaxSegmentSize: 1 << 32, + MaxSegmentsPerTier: 3, + TierGrowth: 3.0, + SegmentsPerMergeTask: 3, + } + err := ValidateMergePlannerOptions(o) + if err != ErrMaxSegmentSizeTooLarge { + t.Error("Validation expected to fail as the MaxSegmentSize exceeds limit") + } +} + +func TestPlanMaxSegmentSizeLimit(t *testing.T) { + o := &MergePlanOptions{ + MaxSegmentSize: 20, + MaxSegmentsPerTier: 5, + TierGrowth: 3.0, + SegmentsPerMergeTask: 5, + FloorSegmentSize: 5, + } + segments := makeLinearSegments(20) + + s := rand.NewSource(time.Now().UnixNano()) + r := rand.New(s) + + max := 20 + min := 5 + randomInRange := func() int64 { + return int64(r.Intn(max-min) + min) + } + for i := 1; i < 20; i++ { + o.MaxSegmentSize = randomInRange() + plans, err := Plan(segments, o) + if err != nil { + t.Errorf("Plan failed, err: %v", err) + } + if len(plans.Tasks) == 0 { + t.Errorf("expected some plans with tasks") + } + + for _, task := range plans.Tasks { + var totalLiveSize int64 + for _, segs := range task.Segments { + totalLiveSize += segs.LiveSize() + } + if totalLiveSize >= o.MaxSegmentSize { + t.Errorf("merged segments size: %d exceeding the MaxSegmentSize"+ + "limit: %d", totalLiveSize, o.MaxSegmentSize) + } + } + } +} + +// ---------------------------------------- + +type testCyclesSpec struct { + descrip string + verbose bool + + n int // Number of cycles to run. + o *MergePlanOptions + + beforePlan func(*testCyclesSpec) + afterPlan func(*testCyclesSpec, *MergePlan) + + cycle int + segments []Segment + nextSegmentId uint64 +} + +func (spec *testCyclesSpec) runCycles(t *testing.T) { + numPlansWithTasks := 0 + + for spec.cycle < spec.n { + if spec.verbose { + emit(spec.descrip, spec.cycle, 0, spec.segments, nil) + } + + if spec.beforePlan != nil { + spec.beforePlan(spec) + } + + if spec.verbose { + emit(spec.descrip, spec.cycle, 1, spec.segments, nil) + } + + plan, err := Plan(spec.segments, spec.o) + if err != nil { + t.Fatalf("expected no err, got: %v", err) + } + + if spec.afterPlan != nil { + spec.afterPlan(spec, plan) + } + + if spec.verbose { + emit(spec.descrip, spec.cycle, 2, spec.segments, plan) + } + + if plan != nil { + if len(plan.Tasks) > 0 { + numPlansWithTasks++ + } + + for _, task := range plan.Tasks { + spec.segments = removeSegments(spec.segments, task.Segments) + + var totLiveSize int64 + for _, segment := range task.Segments { + totLiveSize += segment.LiveSize() + } + + if totLiveSize > 0 { + spec.segments = append(spec.segments, &segment{ + MyId: spec.nextSegmentId, + MyFullSize: totLiveSize, + MyLiveSize: totLiveSize, + }) + spec.nextSegmentId++ + } + } + } + + spec.cycle++ + } + + if numPlansWithTasks <= 0 { + t.Errorf("expected some plans with tasks") + } +} + +func emit(descrip string, cycle int, step int, segments []Segment, plan *MergePlan) { + if os.Getenv("VERBOSE") == "" { + return + } + + suffix := "" + if plan != nil && len(plan.Tasks) > 0 { + suffix = "hasPlan" + } + + fmt.Printf("%s %d.%d ---------- %s\n", descrip, cycle, step, suffix) + fmt.Printf("%s\n", ToBarChart(descrip, 100, segments, plan)) +} + +// ----------------------------------------------------------------------------- +// Test Vector Segment Merging + +func TestPlanMaxSegmentFileSize(t *testing.T) { + tests := []struct { + segments []Segment + o *MergePlanOptions + + expectedTasks [][]uint64 + }{ + { + []Segment{ + &segment{ // ineligible + MyId: 1, + MyFullSize: 4000, + MyLiveSize: 3900, + + MyHasVector: true, + MyFileSize: 3900 * 1000 * 4, // > 2MB + }, + &segment{ // ineligible + MyId: 2, + MyFullSize: 6000, + MyLiveSize: 5500, // > 5000 + + MyHasVector: true, + MyFileSize: 5500 * 1000 * 4, // > 2MB + }, + &segment{ // eligible + MyId: 3, + MyFullSize: 500, + MyLiveSize: 490, + + MyHasVector: true, + MyFileSize: 490 * 1000 * 4, + }, + &segment{ // eligible + MyId: 4, + MyFullSize: 500, + MyLiveSize: 480, + + MyHasVector: true, + MyFileSize: 480 * 1000 * 4, + }, + &segment{ // eligible + MyId: 5, + MyFullSize: 500, + MyLiveSize: 300, + + MyHasVector: true, + MyFileSize: 300 * 1000 * 4, + }, + &segment{ // eligible + MyId: 6, + MyFullSize: 500, + MyLiveSize: 400, + + MyHasVector: true, + MyFileSize: 400 * 1000 * 4, + }, + }, + &MergePlanOptions{ + MaxSegmentSize: 5000, // number of documents + // considering vector dimension as 1000 + // vectorBytes = 5000 * 1000 * 4 = 20MB, which is too large + // So, let's set the fileSize limit to 4MB + MaxSegmentFileSize: 4000000, // 4MB + MaxSegmentsPerTier: 1, + SegmentsPerMergeTask: 2, + TierGrowth: 2.0, + FloorSegmentSize: 1, + }, + [][]uint64{ + {3, 4}, + }, + }, + } + + for testi, test := range tests { + t.Run(fmt.Sprintf("Test-%d", testi), func(t *testing.T) { + plans, err := Plan(test.segments, test.o) + if err != nil { + t.Fatalf("Plan failed, err: %v", err) + } + + for i, task := range plans.Tasks { + var segIDs []uint64 + for _, seg := range task.Segments { + segIDs = append(segIDs, seg.Id()) + } + + if !reflect.DeepEqual(segIDs, test.expectedTasks[0]) { + t.Errorf("expected task segments: %v, got: %v", test.expectedTasks[i], segIDs) + } + } + }) + } +} diff --git a/index/scorch/mergeplan/sort.go b/index/scorch/mergeplan/sort.go new file mode 100644 index 0000000..d044b8d --- /dev/null +++ b/index/scorch/mergeplan/sort.go @@ -0,0 +1,28 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mergeplan + +type byLiveSizeDescending []Segment + +func (a byLiveSizeDescending) Len() int { return len(a) } + +func (a byLiveSizeDescending) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func (a byLiveSizeDescending) Less(i, j int) bool { + if a[i].LiveSize() != a[j].LiveSize() { + return a[i].LiveSize() > a[j].LiveSize() + } + return a[i].Id() < a[j].Id() +} diff --git a/index/scorch/optimize.go b/index/scorch/optimize.go new file mode 100644 index 0000000..389d582 --- /dev/null +++ b/index/scorch/optimize.go @@ -0,0 +1,397 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "sync/atomic" + + "github.com/RoaringBitmap/roaring/v2" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +var OptimizeConjunction = true +var OptimizeConjunctionUnadorned = true +var OptimizeDisjunctionUnadorned = true + +func (s *IndexSnapshotTermFieldReader) Optimize(kind string, + octx index.OptimizableContext) (index.OptimizableContext, error) { + if OptimizeConjunction && kind == "conjunction" { + return s.optimizeConjunction(octx) + } + + if OptimizeConjunctionUnadorned && kind == "conjunction:unadorned" { + return s.optimizeConjunctionUnadorned(octx) + } + + if OptimizeDisjunctionUnadorned && kind == "disjunction:unadorned" { + return s.optimizeDisjunctionUnadorned(octx) + } + + return nil, nil +} + +var OptimizeDisjunctionUnadornedMinChildCardinality = uint64(256) + +// ---------------------------------------------------------------- + +func (s *IndexSnapshotTermFieldReader) optimizeConjunction( + octx index.OptimizableContext) (index.OptimizableContext, error) { + if octx == nil { + octx = &OptimizeTFRConjunction{snapshot: s.snapshot} + } + + o, ok := octx.(*OptimizeTFRConjunction) + if !ok { + return octx, nil + } + + if o.snapshot != s.snapshot { + return nil, fmt.Errorf("tried to optimize conjunction across different snapshots") + } + + o.tfrs = append(o.tfrs, s) + + return o, nil +} + +type OptimizeTFRConjunction struct { + snapshot *IndexSnapshot + + tfrs []*IndexSnapshotTermFieldReader +} + +func (o *OptimizeTFRConjunction) Finish() (index.Optimized, error) { + if len(o.tfrs) <= 1 { + return nil, nil + } + + for i := range o.snapshot.segment { + itr0, ok := o.tfrs[0].iterators[i].(segment.OptimizablePostingsIterator) + if !ok || itr0.ActualBitmap() == nil { + continue + } + + itr1, ok := o.tfrs[1].iterators[i].(segment.OptimizablePostingsIterator) + if !ok || itr1.ActualBitmap() == nil { + continue + } + + bm := roaring.And(itr0.ActualBitmap(), itr1.ActualBitmap()) + + for _, tfr := range o.tfrs[2:] { + itr, ok := tfr.iterators[i].(segment.OptimizablePostingsIterator) + if !ok || itr.ActualBitmap() == nil { + continue + } + + bm.And(itr.ActualBitmap()) + } + + // in this conjunction optimization, the postings iterators + // will all share the same AND'ed together actual bitmap. The + // regular conjunction searcher machinery will still be used, + // but the underlying bitmap will be smaller. + for _, tfr := range o.tfrs { + itr, ok := tfr.iterators[i].(segment.OptimizablePostingsIterator) + if ok && itr.ActualBitmap() != nil { + itr.ReplaceActual(bm) + } + } + } + + return nil, nil +} + +// ---------------------------------------------------------------- + +// An "unadorned" conjunction optimization is appropriate when +// additional or subsidiary information like freq-norm's and +// term-vectors are not required, and instead only the internal-id's +// are needed. +func (s *IndexSnapshotTermFieldReader) optimizeConjunctionUnadorned( + octx index.OptimizableContext) (index.OptimizableContext, error) { + if octx == nil { + octx = &OptimizeTFRConjunctionUnadorned{snapshot: s.snapshot} + } + + o, ok := octx.(*OptimizeTFRConjunctionUnadorned) + if !ok { + return nil, nil + } + + if o.snapshot != s.snapshot { + return nil, fmt.Errorf("tried to optimize unadorned conjunction across different snapshots") + } + + o.tfrs = append(o.tfrs, s) + + return o, nil +} + +type OptimizeTFRConjunctionUnadorned struct { + snapshot *IndexSnapshot + + tfrs []*IndexSnapshotTermFieldReader +} + +var OptimizeTFRConjunctionUnadornedTerm = []byte("") +var OptimizeTFRConjunctionUnadornedField = "*" + +// Finish of an unadorned conjunction optimization will compute a +// termFieldReader with an "actual" bitmap that represents the +// constituent bitmaps AND'ed together. This termFieldReader cannot +// provide any freq-norm or termVector associated information. +func (o *OptimizeTFRConjunctionUnadorned) Finish() (rv index.Optimized, err error) { + if len(o.tfrs) <= 1 { + return nil, nil + } + + // We use an artificial term and field because the optimized + // termFieldReader can represent multiple terms and fields. + oTFR := o.snapshot.unadornedTermFieldReader( + OptimizeTFRConjunctionUnadornedTerm, OptimizeTFRConjunctionUnadornedField) + + var actualBMs []*roaring.Bitmap // Collected from regular posting lists. + +OUTER: + for i := range o.snapshot.segment { + actualBMs = actualBMs[:0] + + var docNum1HitLast uint64 + var docNum1HitLastOk bool + + for _, tfr := range o.tfrs { + if _, ok := tfr.iterators[i].(*emptyPostingsIterator); ok { + // An empty postings iterator means the entire AND is empty. + oTFR.iterators[i] = anEmptyPostingsIterator + continue OUTER + } + + itr, ok := tfr.iterators[i].(segment.OptimizablePostingsIterator) + if !ok { + // We only optimize postings iterators that support this operation. + return nil, nil + } + + // If the postings iterator is "1-hit" optimized, then we + // can perform several optimizations up-front here. + docNum1Hit, ok := itr.DocNum1Hit() + if ok { + if docNum1HitLastOk && docNum1HitLast != docNum1Hit { + // The docNum1Hit doesn't match the previous + // docNum1HitLast, so the entire AND is empty. + oTFR.iterators[i] = anEmptyPostingsIterator + continue OUTER + } + + docNum1HitLast = docNum1Hit + docNum1HitLastOk = true + + continue + } + + if itr.ActualBitmap() == nil { + // An empty actual bitmap means the entire AND is empty. + oTFR.iterators[i] = anEmptyPostingsIterator + continue OUTER + } + + // Collect the actual bitmap for more processing later. + actualBMs = append(actualBMs, itr.ActualBitmap()) + } + + if docNum1HitLastOk { + // We reach here if all the 1-hit optimized posting + // iterators had the same 1-hit docNum, so we can check if + // our collected actual bitmaps also have that docNum. + for _, bm := range actualBMs { + if !bm.Contains(uint32(docNum1HitLast)) { + // The docNum1Hit isn't in one of our actual + // bitmaps, so the entire AND is empty. + oTFR.iterators[i] = anEmptyPostingsIterator + continue OUTER + } + } + + // The actual bitmaps and docNum1Hits all contain or have + // the same 1-hit docNum, so that's our AND'ed result. + oTFR.iterators[i] = newUnadornedPostingsIteratorFrom1Hit(docNum1HitLast) + + continue OUTER + } + + if len(actualBMs) == 0 { + // If we've collected no actual bitmaps at this point, + // then the entire AND is empty. + oTFR.iterators[i] = anEmptyPostingsIterator + continue OUTER + } + + if len(actualBMs) == 1 { + // If we've only 1 actual bitmap, then that's our result. + oTFR.iterators[i] = newUnadornedPostingsIteratorFromBitmap(actualBMs[0]) + + continue OUTER + } + + // Else, AND together our collected bitmaps as our result. + bm := roaring.And(actualBMs[0], actualBMs[1]) + + for _, actualBM := range actualBMs[2:] { + bm.And(actualBM) + } + + oTFR.iterators[i] = newUnadornedPostingsIteratorFromBitmap(bm) + } + + atomic.AddUint64(&o.snapshot.parent.stats.TotTermSearchersStarted, uint64(1)) + return oTFR, nil +} + +// ---------------------------------------------------------------- + +// An "unadorned" disjunction optimization is appropriate when +// additional or subsidiary information like freq-norm's and +// term-vectors are not required, and instead only the internal-id's +// are needed. +func (s *IndexSnapshotTermFieldReader) optimizeDisjunctionUnadorned( + octx index.OptimizableContext) (index.OptimizableContext, error) { + if octx == nil { + octx = &OptimizeTFRDisjunctionUnadorned{ + snapshot: s.snapshot, + } + } + + o, ok := octx.(*OptimizeTFRDisjunctionUnadorned) + if !ok { + return nil, nil + } + + if o.snapshot != s.snapshot { + return nil, fmt.Errorf("tried to optimize unadorned disjunction across different snapshots") + } + + o.tfrs = append(o.tfrs, s) + + return o, nil +} + +type OptimizeTFRDisjunctionUnadorned struct { + snapshot *IndexSnapshot + + tfrs []*IndexSnapshotTermFieldReader +} + +var OptimizeTFRDisjunctionUnadornedTerm = []byte("") +var OptimizeTFRDisjunctionUnadornedField = "*" + +// Finish of an unadorned disjunction optimization will compute a +// termFieldReader with an "actual" bitmap that represents the +// constituent bitmaps OR'ed together. This termFieldReader cannot +// provide any freq-norm or termVector associated information. +func (o *OptimizeTFRDisjunctionUnadorned) Finish() (rv index.Optimized, err error) { + if len(o.tfrs) <= 1 { + return nil, nil + } + + for i := range o.snapshot.segment { + var cMax uint64 + + for _, tfr := range o.tfrs { + itr, ok := tfr.iterators[i].(segment.OptimizablePostingsIterator) + if !ok { + return nil, nil + } + + if itr.ActualBitmap() != nil { + c := itr.ActualBitmap().GetCardinality() + if cMax < c { + cMax = c + } + } + } + } + + // We use an artificial term and field because the optimized + // termFieldReader can represent multiple terms and fields. + oTFR := o.snapshot.unadornedTermFieldReader( + OptimizeTFRDisjunctionUnadornedTerm, OptimizeTFRDisjunctionUnadornedField) + + var docNums []uint32 // Collected docNum's from 1-hit posting lists. + var actualBMs []*roaring.Bitmap // Collected from regular posting lists. + + for i := range o.snapshot.segment { + docNums = docNums[:0] + actualBMs = actualBMs[:0] + + for _, tfr := range o.tfrs { + itr, ok := tfr.iterators[i].(segment.OptimizablePostingsIterator) + if !ok { + return nil, nil + } + + docNum, ok := itr.DocNum1Hit() + if ok { + docNums = append(docNums, uint32(docNum)) + continue + } + + if itr.ActualBitmap() != nil { + actualBMs = append(actualBMs, itr.ActualBitmap()) + } + } + + var bm *roaring.Bitmap + if len(actualBMs) > 2 { + bm = roaring.HeapOr(actualBMs...) + } else if len(actualBMs) == 2 { + bm = roaring.Or(actualBMs[0], actualBMs[1]) + } else if len(actualBMs) == 1 { + bm = actualBMs[0].Clone() + } + + if bm == nil { + bm = roaring.New() + } + + bm.AddMany(docNums) + + oTFR.iterators[i] = newUnadornedPostingsIteratorFromBitmap(bm) + } + + atomic.AddUint64(&o.snapshot.parent.stats.TotTermSearchersStarted, uint64(1)) + return oTFR, nil +} + +// ---------------------------------------------------------------- + +func (i *IndexSnapshot) unadornedTermFieldReader( + term []byte, field string) *IndexSnapshotTermFieldReader { + // This IndexSnapshotTermFieldReader will not be recycled, more + // conversation here: https://github.com/blevesearch/bleve/pull/1438 + return &IndexSnapshotTermFieldReader{ + term: term, + field: field, + snapshot: i, + iterators: make([]segment.PostingsIterator, len(i.segment)), + segmentOffset: 0, + includeFreq: false, + includeNorm: false, + includeTermVectors: false, + recycle: false, + } +} diff --git a/index/scorch/optimize_knn.go b/index/scorch/optimize_knn.go new file mode 100644 index 0000000..3b3bc3d --- /dev/null +++ b/index/scorch/optimize_knn.go @@ -0,0 +1,207 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package scorch + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" + segment_api "github.com/blevesearch/scorch_segment_api/v2" +) + +type OptimizeVR struct { + ctx context.Context + snapshot *IndexSnapshot + totalCost uint64 + // maps field to vector readers + vrs map[string][]*IndexSnapshotVectorReader + // if at least one of the vector readers requires filtered kNN. + requiresFiltering bool +} + +// This setting _MUST_ only be changed during init and not after. +var BleveMaxKNNConcurrency = 10 + +func (o *OptimizeVR) invokeSearcherEndCallback() { + if o.ctx != nil { + if cb := o.ctx.Value(search.SearcherEndCallbackKey); cb != nil { + if cbF, ok := cb.(search.SearcherEndCallbackFn); ok { + if o.totalCost > 0 { + // notify the callback that the searcher creation etc. is finished + // and report back the total cost for it to track and take actions + // appropriately. + _ = cbF(o.totalCost) + } + } + } + } +} + +func (o *OptimizeVR) Finish() error { + // for each field, get the vector index --> invoke the zap func. + // for each VR, populate postings list and iterators + // by passing the obtained vector index and getting similar vectors. + // defer close index - just once. + var errorsM sync.Mutex + var errors []error + + defer o.invokeSearcherEndCallback() + + wg := sync.WaitGroup{} + semaphore := make(chan struct{}, BleveMaxKNNConcurrency) + // Launch goroutines to get vector index for each segment + for i, seg := range o.snapshot.segment { + if sv, ok := seg.segment.(segment_api.VectorSegment); ok { + wg.Add(1) + semaphore <- struct{}{} // Acquire a semaphore slot + go func(index int, segment segment_api.VectorSegment, origSeg *SegmentSnapshot) { + defer func() { + <-semaphore // Release the semaphore slot + wg.Done() + }() + for field, vrs := range o.vrs { + vecIndex, err := segment.InterpretVectorIndex(field, + o.requiresFiltering, origSeg.deleted) + if err != nil { + errorsM.Lock() + errors = append(errors, err) + errorsM.Unlock() + return + } + + // update the vector index size as a meta value in the segment snapshot + vectorIndexSize := vecIndex.Size() + origSeg.cachedMeta.updateMeta(field, vectorIndexSize) + for _, vr := range vrs { + var pl segment_api.VecPostingsList + var err error + + // for each VR, populate postings list and iterators + // by passing the obtained vector index and getting similar vectors. + + // check if the vector reader is configured to use a pre-filter + // to filter out ineligible documents before performing + // kNN search. + if vr.eligibleSelector != nil { + pl, err = vecIndex.SearchWithFilter(vr.vector, vr.k, + vr.eligibleSelector.SegmentEligibleDocs(index), vr.searchParams) + } else { + pl, err = vecIndex.Search(vr.vector, vr.k, vr.searchParams) + } + + if err != nil { + errorsM.Lock() + errors = append(errors, err) + errorsM.Unlock() + go vecIndex.Close() + return + } + + atomic.AddUint64(&o.snapshot.parent.stats.TotKNNSearches, uint64(1)) + + // postings and iterators are already alloc'ed when + // IndexSnapshotVectorReader is created + vr.postings[index] = pl + vr.iterators[index] = pl.Iterator(vr.iterators[index]) + } + go vecIndex.Close() + } + }(i, sv, seg) + } + } + wg.Wait() + close(semaphore) + if len(errors) > 0 { + return errors[0] + } + return nil +} + +func (s *IndexSnapshotVectorReader) VectorOptimize(ctx context.Context, + octx index.VectorOptimizableContext, +) (index.VectorOptimizableContext, error) { + if s.snapshot.parent.segPlugin.Version() < VectorSearchSupportedSegmentVersion { + return nil, fmt.Errorf("vector search not supported for this index, "+ + "index's segment version %v, supported segment version for vector search %v", + s.snapshot.parent.segPlugin.Version(), VectorSearchSupportedSegmentVersion) + } + + if octx == nil { + octx = &OptimizeVR{ + snapshot: s.snapshot, + vrs: make(map[string][]*IndexSnapshotVectorReader), + } + } + + o, ok := octx.(*OptimizeVR) + if !ok { + return octx, nil + } + o.ctx = ctx + if !o.requiresFiltering { + o.requiresFiltering = s.eligibleSelector != nil + } + + if o.snapshot != s.snapshot { + o.invokeSearcherEndCallback() + return nil, fmt.Errorf("tried to optimize KNN across different snapshots") + } + + // for every searcher creation, consult the segment snapshot to see + // what's the vector index size and since you're anyways going + // to use this vector index to perform the search etc. as part of the Finish() + // perform a check as to whether we allow the searcher creation (the downstream) + // Finish() logic to even occur or not. + var sumVectorIndexSize uint64 + for _, seg := range o.snapshot.segment { + vecIndexSize := seg.cachedMeta.fetchMeta(s.field) + if vecIndexSize != nil { + sumVectorIndexSize += vecIndexSize.(uint64) + } + } + + if o.ctx != nil { + if cb := o.ctx.Value(search.SearcherStartCallbackKey); cb != nil { + if cbF, ok := cb.(search.SearcherStartCallbackFn); ok { + err := cbF(sumVectorIndexSize) + if err != nil { + // it's important to invoke the end callback at this point since + // if the earlier searchers of this optimze struct were successful + // the cost corresponding to it would be incremented and if the + // current searcher fails the check then we end up erroring out + // the overall optimized searcher creation, the cost needs to be + // handled appropriately. + o.invokeSearcherEndCallback() + return nil, err + } + } + } + } + + // total cost is essentially the sum of the vector indexes' size across all the + // searchers - all of them end up reading and maintaining a vector index. + // misacconting this value would end up calling the "end" callback with a value + // not equal to the value passed to "start" callback. + o.totalCost += sumVectorIndexSize + o.vrs[s.field] = append(o.vrs[s.field], s) + return o, nil +} diff --git a/index/scorch/persister.go b/index/scorch/persister.go new file mode 100644 index 0000000..3aca020 --- /dev/null +++ b/index/scorch/persister.go @@ -0,0 +1,1445 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "log" + "math" + "os" + "path/filepath" + "slices" + "sort" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/RoaringBitmap/roaring/v2" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" + bolt "go.etcd.io/bbolt" +) + +// DefaultPersisterNapTimeMSec is kept to zero as this helps in direct +// persistence of segments with the default safe batch option. +// If the default safe batch option results in high number of +// files on disk, then users may initialise this configuration parameter +// with higher values so that the persister will nap a bit within it's +// work loop to favour better in-memory merging of segments to result +// in fewer segment files on disk. But that may come with an indexing +// performance overhead. +// Unsafe batch users are advised to override this to higher value +// for better performance especially with high data density. +var DefaultPersisterNapTimeMSec int = 0 // ms + +// DefaultPersisterNapUnderNumFiles helps in controlling the pace of +// persister. At times of a slow merger progress with heavy file merging +// operations, its better to pace down the persister for letting the merger +// to catch up within a range defined by this parameter. +// Fewer files on disk (as per the merge plan) would result in keeping the +// file handle usage under limit, faster disk merger and a healthier index. +// Its been observed that such a loosely sync'ed introducer-persister-merger +// trio results in better overall performance. +var DefaultPersisterNapUnderNumFiles int = 1000 + +var DefaultMemoryPressurePauseThreshold uint64 = math.MaxUint64 + +type persisterOptions struct { + // PersisterNapTimeMSec controls the wait/delay injected into + // persistence workloop to improve the chances for + // a healthier and heavier in-memory merging + PersisterNapTimeMSec int + + // PersisterNapTimeMSec > 0, and the number of files is less than + // PersisterNapUnderNumFiles, then the persister will sleep + // PersisterNapTimeMSec amount of time to improve the chances for + // a healthier and heavier in-memory merging + PersisterNapUnderNumFiles int + + // MemoryPressurePauseThreshold let persister to have a better leeway + // for prudently performing the memory merge of segments on a memory + // pressure situation. Here the config value is an upper threshold + // for the number of paused application threads. The default value would + // be a very high number to always favour the merging of memory segments. + MemoryPressurePauseThreshold uint64 + + // NumPersisterWorkers decides the number of parallel workers that will + // perform the in-memory merge of segments followed by a flush operation. + NumPersisterWorkers int + + // MaxSizeInMemoryMerge is the maximum size of data that a single persister + // worker is allowed to work on + MaxSizeInMemoryMergePerWorker int +} + +type notificationChan chan struct{} + +func (s *Scorch) persisterLoop() { + defer func() { + if r := recover(); r != nil { + s.fireAsyncError(&AsyncPanicError{ + Source: "persister", + Path: s.path, + }) + } + + s.asyncTasks.Done() + }() + + var persistWatchers []*epochWatcher + var lastPersistedEpoch, lastMergedEpoch uint64 + var ew *epochWatcher + + var unpersistedCallbacks []index.BatchCallback + + po, err := s.parsePersisterOptions() + if err != nil { + s.fireAsyncError(fmt.Errorf("persisterOptions json parsing err: %v", err)) + return + } + +OUTER: + for { + atomic.AddUint64(&s.stats.TotPersistLoopBeg, 1) + + select { + case <-s.closeCh: + break OUTER + case ew = <-s.persisterNotifier: + persistWatchers = append(persistWatchers, ew) + default: + } + if ew != nil && ew.epoch > lastMergedEpoch { + lastMergedEpoch = ew.epoch + } + lastMergedEpoch, persistWatchers = s.pausePersisterForMergerCatchUp(lastPersistedEpoch, + lastMergedEpoch, persistWatchers, po) + + var ourSnapshot *IndexSnapshot + var ourPersisted []chan error + var ourPersistedCallbacks []index.BatchCallback + + // check to see if there is a new snapshot to persist + s.rootLock.Lock() + if s.root != nil && s.root.epoch > lastPersistedEpoch { + ourSnapshot = s.root + ourSnapshot.AddRef() + ourPersisted = s.rootPersisted + s.rootPersisted = nil + ourPersistedCallbacks = s.persistedCallbacks + s.persistedCallbacks = nil + atomic.StoreUint64(&s.iStats.persistSnapshotSize, uint64(ourSnapshot.Size())) + atomic.StoreUint64(&s.iStats.persistEpoch, ourSnapshot.epoch) + } + s.rootLock.Unlock() + + if ourSnapshot != nil { + startTime := time.Now() + + err := s.persistSnapshot(ourSnapshot, po) + for _, ch := range ourPersisted { + if err != nil { + ch <- err + } + close(ch) + } + if err != nil { + atomic.StoreUint64(&s.iStats.persistEpoch, 0) + if err == segment.ErrClosed { + // index has been closed + _ = ourSnapshot.DecRef() + break OUTER + } + + // save this current snapshot's persistedCallbacks, to invoke during + // the retry attempt + unpersistedCallbacks = append(unpersistedCallbacks, ourPersistedCallbacks...) + + s.fireAsyncError(fmt.Errorf("got err persisting snapshot: %v", err)) + _ = ourSnapshot.DecRef() + atomic.AddUint64(&s.stats.TotPersistLoopErr, 1) + continue OUTER + } + + if unpersistedCallbacks != nil { + // in the event of this being a retry attempt for persisting a snapshot + // that had earlier failed, prepend the persistedCallbacks associated + // with earlier segment(s) to the latest persistedCallbacks + ourPersistedCallbacks = append(unpersistedCallbacks, ourPersistedCallbacks...) + unpersistedCallbacks = nil + } + + for i := range ourPersistedCallbacks { + ourPersistedCallbacks[i](err) + } + + atomic.StoreUint64(&s.stats.LastPersistedEpoch, ourSnapshot.epoch) + + lastPersistedEpoch = ourSnapshot.epoch + for _, ew := range persistWatchers { + close(ew.notifyCh) + } + + persistWatchers = nil + _ = ourSnapshot.DecRef() + + changed := false + s.rootLock.RLock() + if s.root != nil && s.root.epoch != lastPersistedEpoch { + changed = true + } + s.rootLock.RUnlock() + + s.fireEvent(EventKindPersisterProgress, time.Since(startTime)) + + if changed { + atomic.AddUint64(&s.stats.TotPersistLoopProgress, 1) + continue OUTER + } + } + + // tell the introducer we're waiting for changes + w := &epochWatcher{ + epoch: lastPersistedEpoch, + notifyCh: make(notificationChan, 1), + } + + select { + case <-s.closeCh: + break OUTER + case s.introducerNotifier <- w: + } + + if ok := s.fireEvent(EventKindPurgerCheck, 0); ok { + s.removeOldData() // might as well cleanup while waiting + } + + atomic.AddUint64(&s.stats.TotPersistLoopWait, 1) + + select { + case <-s.closeCh: + break OUTER + case <-w.notifyCh: + // woken up, next loop should pick up work + atomic.AddUint64(&s.stats.TotPersistLoopWaitNotified, 1) + case ew = <-s.persisterNotifier: + // if the watchers are already caught up then let them wait, + // else let them continue to do the catch up + persistWatchers = append(persistWatchers, ew) + } + + atomic.AddUint64(&s.stats.TotPersistLoopEnd, 1) + } +} + +func notifyMergeWatchers(lastPersistedEpoch uint64, + persistWatchers []*epochWatcher, +) []*epochWatcher { + var watchersNext []*epochWatcher + for _, w := range persistWatchers { + if w.epoch < lastPersistedEpoch { + close(w.notifyCh) + } else { + watchersNext = append(watchersNext, w) + } + } + return watchersNext +} + +func (s *Scorch) pausePersisterForMergerCatchUp(lastPersistedEpoch uint64, + lastMergedEpoch uint64, persistWatchers []*epochWatcher, + po *persisterOptions, +) (uint64, []*epochWatcher) { + // First, let the watchers proceed if they lag behind + persistWatchers = notifyMergeWatchers(lastPersistedEpoch, persistWatchers) + + // Check the merger lag by counting the segment files on disk, + numFilesOnDisk, _, _ := s.diskFileStats(nil) + + // On finding fewer files on disk, persister takes a short pause + // for sufficient in-memory segments to pile up for the next + // memory merge cum persist loop. + if numFilesOnDisk < uint64(po.PersisterNapUnderNumFiles) && + po.PersisterNapTimeMSec > 0 && s.NumEventsBlocking() == 0 { + select { + case <-s.closeCh: + case <-time.After(time.Millisecond * time.Duration(po.PersisterNapTimeMSec)): + atomic.AddUint64(&s.stats.TotPersisterNapPauseCompleted, 1) + + case ew := <-s.persisterNotifier: + // unblock the merger in meantime + persistWatchers = append(persistWatchers, ew) + lastMergedEpoch = ew.epoch + persistWatchers = notifyMergeWatchers(lastPersistedEpoch, persistWatchers) + atomic.AddUint64(&s.stats.TotPersisterMergerNapBreak, 1) + } + return lastMergedEpoch, persistWatchers + } + + // Finding too many files on disk could be due to two reasons. + // 1. Too many older snapshots awaiting the clean up. + // 2. The merger could be lagging behind on merging the disk files. + if numFilesOnDisk > uint64(po.PersisterNapUnderNumFiles) { + if ok := s.fireEvent(EventKindPurgerCheck, 0); ok { + s.removeOldData() + } + numFilesOnDisk, _, _ = s.diskFileStats(nil) + } + + // Persister pause until the merger catches up to reduce the segment + // file count under the threshold. + // But if there is memory pressure, then skip this sleep maneuvers. +OUTER: + for po.PersisterNapUnderNumFiles > 0 && + numFilesOnDisk >= uint64(po.PersisterNapUnderNumFiles) && + lastMergedEpoch < lastPersistedEpoch { + atomic.AddUint64(&s.stats.TotPersisterSlowMergerPause, 1) + + select { + case <-s.closeCh: + break OUTER + case ew := <-s.persisterNotifier: + persistWatchers = append(persistWatchers, ew) + lastMergedEpoch = ew.epoch + } + + atomic.AddUint64(&s.stats.TotPersisterSlowMergerResume, 1) + + // let the watchers proceed if they lag behind + persistWatchers = notifyMergeWatchers(lastPersistedEpoch, persistWatchers) + + numFilesOnDisk, _, _ = s.diskFileStats(nil) + } + + return lastMergedEpoch, persistWatchers +} + +func (s *Scorch) parsePersisterOptions() (*persisterOptions, error) { + po := persisterOptions{ + PersisterNapTimeMSec: DefaultPersisterNapTimeMSec, + PersisterNapUnderNumFiles: DefaultPersisterNapUnderNumFiles, + MemoryPressurePauseThreshold: DefaultMemoryPressurePauseThreshold, + NumPersisterWorkers: DefaultNumPersisterWorkers, + MaxSizeInMemoryMergePerWorker: DefaultMaxSizeInMemoryMergePerWorker, + } + if v, ok := s.config["scorchPersisterOptions"]; ok { + b, err := util.MarshalJSON(v) + if err != nil { + return &po, err + } + + err = util.UnmarshalJSON(b, &po) + if err != nil { + return &po, err + } + } + return &po, nil +} + +func (s *Scorch) persistSnapshot(snapshot *IndexSnapshot, + po *persisterOptions, +) error { + // Perform in-memory segment merging only when the memory pressure is + // below the configured threshold, else the persister performs the + // direct persistence of segments. + if s.NumEventsBlocking() < po.MemoryPressurePauseThreshold { + persisted, err := s.persistSnapshotMaybeMerge(snapshot, po) + if err != nil { + return err + } + if persisted { + return nil + } + } + + return s.persistSnapshotDirect(snapshot, nil) +} + +// DefaultMinSegmentsForInMemoryMerge represents the default number of +// in-memory zap segments that persistSnapshotMaybeMerge() needs to +// see in an IndexSnapshot before it decides to merge and persist +// those segments +var DefaultMinSegmentsForInMemoryMerge = 2 + +type flushable struct { + segments []segment.Segment + drops []*roaring.Bitmap + sbIdxs []int + totDocs uint64 +} + +// number workers which parallely perform an in-memory merge of the segments +// followed by a flush operation. +var DefaultNumPersisterWorkers = 1 + +// maximum size of data that a single worker is allowed to perform the in-memory +// merge operation. +var DefaultMaxSizeInMemoryMergePerWorker = 0 + +func legacyFlushBehaviour(maxSizeInMemoryMergePerWorker, numPersisterWorkers int) bool { + // DefaultMaxSizeInMemoryMergePerWorker = 0 is a special value to preserve the leagcy + // one-shot in-memory merge + flush behaviour. + return maxSizeInMemoryMergePerWorker == 0 && numPersisterWorkers == 1 +} + +// persistSnapshotMaybeMerge examines the snapshot and might merge and +// persist the in-memory zap segments if there are enough of them +func (s *Scorch) persistSnapshotMaybeMerge(snapshot *IndexSnapshot, po *persisterOptions) ( + bool, error) { + // collect the in-memory zap segments (SegmentBase instances) + var sbs []segment.Segment + var sbsDrops []*roaring.Bitmap + var sbsIndexes []int + var oldSegIdxs []int + + flushSet := make([]*flushable, 0) + var totSize int + var numSegsToFlushOut int + var totDocs uint64 + + // legacy behaviour of merge + flush of all in-memory segments in one-shot + if legacyFlushBehaviour(po.MaxSizeInMemoryMergePerWorker, po.NumPersisterWorkers) { + val := &flushable{ + segments: make([]segment.Segment, 0), + drops: make([]*roaring.Bitmap, 0), + sbIdxs: make([]int, 0), + totDocs: totDocs, + } + for i, snapshot := range snapshot.segment { + if _, ok := snapshot.segment.(segment.PersistedSegment); !ok { + val.segments = append(val.segments, snapshot.segment) + val.drops = append(val.drops, snapshot.deleted) + val.sbIdxs = append(val.sbIdxs, i) + oldSegIdxs = append(oldSegIdxs, i) + val.totDocs += snapshot.segment.Count() + numSegsToFlushOut++ + } + } + + flushSet = append(flushSet, val) + } else { + // constructs a flushSet where each flushable object contains a set of segments + // to be merged and flushed out to disk. + for i, snapshot := range snapshot.segment { + if totSize >= po.MaxSizeInMemoryMergePerWorker && + len(sbs) >= DefaultMinSegmentsForInMemoryMerge { + numSegsToFlushOut += len(sbs) + val := &flushable{ + segments: slices.Clone(sbs), + drops: slices.Clone(sbsDrops), + sbIdxs: slices.Clone(sbsIndexes), + totDocs: totDocs, + } + flushSet = append(flushSet, val) + oldSegIdxs = append(oldSegIdxs, sbsIndexes...) + + sbs, sbsDrops, sbsIndexes = sbs[:0], sbsDrops[:0], sbsIndexes[:0] + totSize, totDocs = 0, 0 + } + + if len(flushSet) >= int(po.NumPersisterWorkers) { + break + } + + if _, ok := snapshot.segment.(segment.PersistedSegment); !ok { + sbs = append(sbs, snapshot.segment) + sbsDrops = append(sbsDrops, snapshot.deleted) + sbsIndexes = append(sbsIndexes, i) + totDocs += snapshot.segment.Count() + totSize += snapshot.segment.Size() + } + } + // if there were too few segments just merge them all as part of a single worker + if len(flushSet) < po.NumPersisterWorkers { + numSegsToFlushOut += len(sbs) + val := &flushable{ + segments: slices.Clone(sbs), + drops: slices.Clone(sbsDrops), + sbIdxs: slices.Clone(sbsIndexes), + totDocs: totDocs, + } + flushSet = append(flushSet, val) + oldSegIdxs = append(oldSegIdxs, sbsIndexes...) + } + } + + if numSegsToFlushOut < DefaultMinSegmentsForInMemoryMerge { + return false, nil + } + + // the newSnapshot at this point would contain the newly created file segments + // and updated with the root. + newSnapshot, newSegmentIDs, err := s.mergeAndPersistInMemorySegments(snapshot, flushSet) + if err != nil { + return false, err + } + + if newSnapshot == nil { + return false, nil + } + + defer func() { + _ = newSnapshot.DecRef() + }() + + mergedSegmentIDs := map[uint64]struct{}{} + for _, idx := range oldSegIdxs { + mergedSegmentIDs[snapshot.segment[idx].id] = struct{}{} + } + + newMergedSegmentIDs := make(map[uint64]struct{}, len(newSegmentIDs)) + for _, id := range newSegmentIDs { + newMergedSegmentIDs[id] = struct{}{} + } + + // construct a snapshot that's logically equivalent to the input + // snapshot, but with merged segments replaced by the new segment + equiv := &IndexSnapshot{ + parent: snapshot.parent, + segment: make([]*SegmentSnapshot, 0, len(snapshot.segment)), + internal: snapshot.internal, + epoch: snapshot.epoch, + creator: "persistSnapshotMaybeMerge", + } + + // to track which segments haven't participated in the in-memory merge + // they won't be flushed out to the disk yet, but in the next cycle will be + // merged in-memory and then flushed out - this is to keep the number of + // on-disk files in limit. + exclude := make(map[uint64]struct{}) + + // copy to the equiv the segments that weren't replaced + for _, segment := range snapshot.segment { + if _, wasMerged := mergedSegmentIDs[segment.id]; !wasMerged { + equiv.segment = append(equiv.segment, segment) + exclude[segment.id] = struct{}{} + } + } + + // append to the equiv the newly merged segments + for _, segment := range newSnapshot.segment { + if _, ok := newMergedSegmentIDs[segment.id]; ok { + equiv.segment = append(equiv.segment, &SegmentSnapshot{ + id: segment.id, + segment: segment.segment, + deleted: nil, // nil since merging handled deletions + stats: nil, + }) + } + } + + err = s.persistSnapshotDirect(equiv, exclude) + if err != nil { + return false, err + } + + return true, nil +} + +func copyToDirectory(srcPath string, d index.Directory) (int64, error) { + if d == nil { + return 0, nil + } + + dest, err := d.GetWriter(filepath.Join("store", filepath.Base(srcPath))) + if err != nil { + return 0, fmt.Errorf("GetWriter err: %v", err) + } + + sourceFileStat, err := os.Stat(srcPath) + if err != nil { + return 0, err + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", srcPath) + } + + source, err := os.Open(srcPath) + if err != nil { + return 0, err + } + defer source.Close() + defer dest.Close() + return io.Copy(dest, source) +} + +func persistToDirectory(seg segment.UnpersistedSegment, d index.Directory, + path string, +) error { + if d == nil { + return seg.Persist(path) + } + + sg, ok := seg.(io.WriterTo) + if !ok { + return fmt.Errorf("no io.WriterTo segment implementation found") + } + + w, err := d.GetWriter(filepath.Join("store", filepath.Base(path))) + if err != nil { + return err + } + + _, err = sg.WriteTo(w) + w.Close() + + return err +} + +func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string, + segPlugin SegmentPlugin, exclude map[uint64]struct{}, d index.Directory) ( + []string, map[uint64]string, error) { + snapshotsBucket, err := tx.CreateBucketIfNotExists(boltSnapshotsBucket) + if err != nil { + return nil, nil, err + } + newSnapshotKey := encodeUvarintAscending(nil, snapshot.epoch) + snapshotBucket, err := snapshotsBucket.CreateBucketIfNotExists(newSnapshotKey) + if err != nil { + return nil, nil, err + } + + // persist meta values + metaBucket, err := snapshotBucket.CreateBucketIfNotExists(boltMetaDataKey) + if err != nil { + return nil, nil, err + } + err = metaBucket.Put(boltMetaDataSegmentTypeKey, []byte(segPlugin.Type())) + if err != nil { + return nil, nil, err + } + buf := make([]byte, binary.MaxVarintLen32) + binary.BigEndian.PutUint32(buf, segPlugin.Version()) + err = metaBucket.Put(boltMetaDataSegmentVersionKey, buf) + if err != nil { + return nil, nil, err + } + + // Storing the timestamp at which the current indexSnapshot + // was persisted, useful when you want to spread the + // numSnapshotsToKeep reasonably better than consecutive + // epochs. + currTimeStamp := time.Now() + timeStampBinary, err := currTimeStamp.MarshalText() + if err != nil { + return nil, nil, err + } + err = metaBucket.Put(boltMetaDataTimeStamp, timeStampBinary) + if err != nil { + return nil, nil, err + } + + // persist internal values + internalBucket, err := snapshotBucket.CreateBucketIfNotExists(boltInternalKey) + if err != nil { + return nil, nil, err + } + // TODO optimize writing these in order? + for k, v := range snapshot.internal { + err = internalBucket.Put([]byte(k), v) + if err != nil { + return nil, nil, err + } + } + + if snapshot.parent != nil { + val := make([]byte, 8) + bytesWritten := atomic.LoadUint64(&snapshot.parent.stats.TotBytesWrittenAtIndexTime) + binary.LittleEndian.PutUint64(val, bytesWritten) + err = internalBucket.Put(TotBytesWrittenKey, val) + if err != nil { + return nil, nil, err + } + } + + filenames := make([]string, 0, len(snapshot.segment)) + newSegmentPaths := make(map[uint64]string, len(snapshot.segment)) + + // first ensure that each segment in this snapshot has been persisted + for _, segmentSnapshot := range snapshot.segment { + snapshotSegmentKey := encodeUvarintAscending(nil, segmentSnapshot.id) + snapshotSegmentBucket, err := snapshotBucket.CreateBucketIfNotExists(snapshotSegmentKey) + if err != nil { + return nil, nil, err + } + switch seg := segmentSnapshot.segment.(type) { + case segment.PersistedSegment: + segPath := seg.Path() + _, err = copyToDirectory(segPath, d) + if err != nil { + return nil, nil, fmt.Errorf("segment: %s copy err: %v", segPath, err) + } + filename := filepath.Base(segPath) + err = snapshotSegmentBucket.Put(boltPathKey, []byte(filename)) + if err != nil { + return nil, nil, err + } + filenames = append(filenames, filename) + case segment.UnpersistedSegment: + // need to persist this to disk if its not part of exclude list (which + // restricts which in-memory segment to be persisted to disk) + if _, ok := exclude[segmentSnapshot.id]; !ok { + filename := zapFileName(segmentSnapshot.id) + path := filepath.Join(path, filename) + err := persistToDirectory(seg, d, path) + if err != nil { + return nil, nil, fmt.Errorf("segment: %s persist err: %v", path, err) + } + newSegmentPaths[segmentSnapshot.id] = path + err = snapshotSegmentBucket.Put(boltPathKey, []byte(filename)) + if err != nil { + return nil, nil, err + } + filenames = append(filenames, filename) + } + default: + return nil, nil, fmt.Errorf("unknown segment type: %T", seg) + } + // store current deleted bits + var roaringBuf bytes.Buffer + if segmentSnapshot.deleted != nil { + _, err = segmentSnapshot.deleted.WriteTo(&roaringBuf) + if err != nil { + return nil, nil, fmt.Errorf("error persisting roaring bytes: %v", err) + } + err = snapshotSegmentBucket.Put(boltDeletedKey, roaringBuf.Bytes()) + if err != nil { + return nil, nil, err + } + } + + // store segment stats + if segmentSnapshot.stats != nil { + b, err := json.Marshal(segmentSnapshot.stats.Fetch()) + if err != nil { + return nil, nil, err + } + err = snapshotSegmentBucket.Put(boltStatsKey, b) + if err != nil { + return nil, nil, err + } + } + } + + return filenames, newSegmentPaths, nil +} + +func (s *Scorch) persistSnapshotDirect(snapshot *IndexSnapshot, exclude map[uint64]struct{}) (err error) { + // start a write transaction + tx, err := s.rootBolt.Begin(true) + if err != nil { + return err + } + // defer rollback on error + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + + filenames, newSegmentPaths, err := prepareBoltSnapshot(snapshot, tx, s.path, s.segPlugin, exclude, nil) + if err != nil { + return err + } + + // we need to swap in a new root only when we've persisted 1 or + // more segments -- whereby the new root would have 1-for-1 + // replacements of in-memory segments with file-based segments + // + // other cases like updates to internal values only, and/or when + // there are only deletions, are already covered and persisted by + // the newly populated boltdb snapshotBucket above + if len(newSegmentPaths) > 0 { + // now try to open all the new snapshots + newSegments := make(map[uint64]segment.Segment, len(newSegmentPaths)) + defer func() { + for _, s := range newSegments { + if s != nil { + // cleanup segments that were opened but not + // swapped into the new root + _ = s.Close() + } + } + }() + for segmentID, path := range newSegmentPaths { + newSegments[segmentID], err = s.segPlugin.Open(path) + if err != nil { + return fmt.Errorf("error opening new segment at %s, %v", path, err) + } + } + + persist := &persistIntroduction{ + persisted: newSegments, + applied: make(notificationChan), + } + + select { + case <-s.closeCh: + return segment.ErrClosed + case s.persists <- persist: + } + + select { + case <-s.closeCh: + return segment.ErrClosed + case <-persist.applied: + } + } + + err = tx.Commit() + if err != nil { + return err + } + + err = s.rootBolt.Sync() + if err != nil { + return err + } + + // allow files to become eligible for removal after commit, such + // as file segments from snapshots that came from the merger + s.rootLock.Lock() + for _, filename := range filenames { + delete(s.ineligibleForRemoval, filename) + } + s.rootLock.Unlock() + + return nil +} + +func zapFileName(epoch uint64) string { + return fmt.Sprintf("%012x.zap", epoch) +} + +// bolt snapshot code + +var ( + boltSnapshotsBucket = []byte{'s'} + boltPathKey = []byte{'p'} + boltDeletedKey = []byte{'d'} + boltInternalKey = []byte{'i'} + boltMetaDataKey = []byte{'m'} + boltMetaDataSegmentTypeKey = []byte("type") + boltMetaDataSegmentVersionKey = []byte("version") + boltMetaDataTimeStamp = []byte("timeStamp") + boltStatsKey = []byte("stats") + TotBytesWrittenKey = []byte("TotBytesWritten") +) + +func (s *Scorch) loadFromBolt() error { + err := s.rootBolt.View(func(tx *bolt.Tx) error { + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil + } + foundRoot := false + c := snapshots.Cursor() + for k, _ := c.Last(); k != nil; k, _ = c.Prev() { + _, snapshotEpoch, err := decodeUvarintAscending(k) + if err != nil { + log.Printf("unable to parse segment epoch %x, continuing", k) + continue + } + if foundRoot { + s.AddEligibleForRemoval(snapshotEpoch) + continue + } + snapshot := snapshots.Bucket(k) + if snapshot == nil { + log.Printf("snapshot key, but bucket missing %x, continuing", k) + s.AddEligibleForRemoval(snapshotEpoch) + continue + } + indexSnapshot, err := s.loadSnapshot(snapshot) + if err != nil { + log.Printf("unable to load snapshot, %v, continuing", err) + s.AddEligibleForRemoval(snapshotEpoch) + continue + } + indexSnapshot.epoch = snapshotEpoch + // set the nextSegmentID + s.nextSegmentID, err = s.maxSegmentIDOnDisk() + if err != nil { + return err + } + s.nextSegmentID++ + s.rootLock.Lock() + s.nextSnapshotEpoch = snapshotEpoch + 1 + rootPrev := s.root + s.root = indexSnapshot + s.rootLock.Unlock() + + if rootPrev != nil { + _ = rootPrev.DecRef() + } + + foundRoot = true + } + return nil + }) + if err != nil { + return err + } + + persistedSnapshots, err := s.rootBoltSnapshotMetaData() + if err != nil { + return err + } + s.checkPoints = persistedSnapshots + return nil +} + +// LoadSnapshot loads the segment with the specified epoch +// NOTE: this is currently ONLY intended to be used by the command-line tool +func (s *Scorch) LoadSnapshot(epoch uint64) (rv *IndexSnapshot, err error) { + err = s.rootBolt.View(func(tx *bolt.Tx) error { + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil + } + snapshotKey := encodeUvarintAscending(nil, epoch) + snapshot := snapshots.Bucket(snapshotKey) + if snapshot == nil { + return fmt.Errorf("snapshot with epoch: %v - doesn't exist", epoch) + } + rv, err = s.loadSnapshot(snapshot) + return err + }) + if err != nil { + return nil, err + } + return rv, nil +} + +func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) { + rv := &IndexSnapshot{ + parent: s, + internal: make(map[string][]byte), + refs: 1, + creator: "loadSnapshot", + } + // first we look for the meta-data bucket, this will tell us + // which segment type/version was used for this snapshot + // all operations for this scorch will use this type/version + metaBucket := snapshot.Bucket(boltMetaDataKey) + if metaBucket == nil { + _ = rv.DecRef() + return nil, fmt.Errorf("meta-data bucket missing") + } + segmentType := string(metaBucket.Get(boltMetaDataSegmentTypeKey)) + segmentVersion := binary.BigEndian.Uint32( + metaBucket.Get(boltMetaDataSegmentVersionKey)) + err := s.loadSegmentPlugin(segmentType, segmentVersion) + if err != nil { + _ = rv.DecRef() + return nil, fmt.Errorf( + "unable to load correct segment wrapper: %v", err) + } + var running uint64 + c := snapshot.Cursor() + for k, _ := c.First(); k != nil; k, _ = c.Next() { + if k[0] == boltInternalKey[0] { + internalBucket := snapshot.Bucket(k) + if internalBucket == nil { + _ = rv.DecRef() + return nil, fmt.Errorf("internal bucket missing") + } + err := internalBucket.ForEach(func(key []byte, val []byte) error { + copiedVal := append([]byte(nil), val...) + rv.internal[string(key)] = copiedVal + return nil + }) + if err != nil { + _ = rv.DecRef() + return nil, err + } + } else if k[0] != boltMetaDataKey[0] { + segmentBucket := snapshot.Bucket(k) + if segmentBucket == nil { + _ = rv.DecRef() + return nil, fmt.Errorf("segment key, but bucket missing % x", k) + } + segmentSnapshot, err := s.loadSegment(segmentBucket) + if err != nil { + _ = rv.DecRef() + return nil, fmt.Errorf("failed to load segment: %v", err) + } + _, segmentSnapshot.id, err = decodeUvarintAscending(k) + if err != nil { + _ = rv.DecRef() + return nil, fmt.Errorf("failed to decode segment id: %v", err) + } + rv.segment = append(rv.segment, segmentSnapshot) + rv.offsets = append(rv.offsets, running) + running += segmentSnapshot.segment.Count() + } + } + return rv, nil +} + +func (s *Scorch) loadSegment(segmentBucket *bolt.Bucket) (*SegmentSnapshot, error) { + pathBytes := segmentBucket.Get(boltPathKey) + if pathBytes == nil { + return nil, fmt.Errorf("segment path missing") + } + segmentPath := s.path + string(os.PathSeparator) + string(pathBytes) + segment, err := s.segPlugin.Open(segmentPath) + if err != nil { + return nil, fmt.Errorf("error opening bolt segment: %v", err) + } + + rv := &SegmentSnapshot{ + segment: segment, + cachedDocs: &cachedDocs{cache: nil}, + cachedMeta: &cachedMeta{meta: nil}, + } + deletedBytes := segmentBucket.Get(boltDeletedKey) + if deletedBytes != nil { + deletedBitmap := roaring.NewBitmap() + r := bytes.NewReader(deletedBytes) + _, err := deletedBitmap.ReadFrom(r) + if err != nil { + _ = segment.Close() + return nil, fmt.Errorf("error reading deleted bytes: %v", err) + } + if !deletedBitmap.IsEmpty() { + rv.deleted = deletedBitmap + } + } + statBytes := segmentBucket.Get(boltStatsKey) + if statBytes != nil { + var statsMap map[string]map[string]uint64 + + err := json.Unmarshal(statBytes, &statsMap) + stats := &fieldStats{statMap: statsMap} + if err != nil { + _ = segment.Close() + return nil, fmt.Errorf("error reading stat bytes: %v", err) + } + rv.stats = stats + } + + return rv, nil +} + +func (s *Scorch) removeOldData() { + removed, err := s.removeOldBoltSnapshots() + if err != nil { + s.fireAsyncError(fmt.Errorf("got err removing old bolt snapshots: %v", err)) + } + atomic.AddUint64(&s.stats.TotSnapshotsRemovedFromMetaStore, uint64(removed)) + + err = s.removeOldZapFiles() + if err != nil { + s.fireAsyncError(fmt.Errorf("got err removing old zap files: %v", err)) + } +} + +// NumSnapshotsToKeep represents how many recent, old snapshots to +// keep around per Scorch instance. Useful for apps that require +// rollback'ability. +var NumSnapshotsToKeep = 1 + +// RollbackSamplingInterval controls how far back we are looking +// in the history to get the rollback points. +// For example, a value of 10 minutes ensures that the +// protected snapshots (NumSnapshotsToKeep = 3) are: +// +// the very latest snapshot(ie the current one), +// the snapshot that was persisted 10 minutes before the current one, +// the snapshot that was persisted 20 minutes before the current one +// +// By default however, the timeseries way of protecting snapshots is +// disabled, and we protect the latest three contiguous snapshots +var RollbackSamplingInterval = 0 * time.Minute + +// Controls what portion of the earlier rollback points to retain during +// a infrequent/sparse mutation scenario +var RollbackRetentionFactor = float64(0.5) + +func getTimeSeriesSnapshots(maxDataPoints int, interval time.Duration, + snapshots []*snapshotMetaData, +) (int, map[uint64]time.Time) { + if interval == 0 { + return len(snapshots), map[uint64]time.Time{} + } + // the map containing the time series snapshots, i.e the timeseries of snapshots + // each of which is separated by rollbackSamplingInterval + rv := make(map[uint64]time.Time) + // the last point in the "time series", i.e. the timeseries of snapshots + // each of which is separated by rollbackSamplingInterval + ptr := len(snapshots) - 1 + rv[snapshots[ptr].epoch] = snapshots[ptr].timeStamp + numSnapshotsProtected := 1 + + // traverse the list in reverse order, older timestamps to newer ones. + for i := ptr - 1; i >= 0; i-- { + // If we find a timeStamp which is the next datapoint in our + // timeseries of snapshots, and newer by RollbackSamplingInterval duration + // (comparison in terms of minutes), which is the interval of our time + // series. In this case, add the epoch rv + if snapshots[i].timeStamp.Sub(snapshots[ptr].timeStamp).Minutes() > + interval.Minutes() { + if _, ok := rv[snapshots[i+1].epoch]; !ok { + rv[snapshots[i+1].epoch] = snapshots[i+1].timeStamp + ptr = i + 1 + numSnapshotsProtected++ + } + } else if snapshots[i].timeStamp.Sub(snapshots[ptr].timeStamp).Minutes() == + interval.Minutes() { + if _, ok := rv[snapshots[i].epoch]; !ok { + rv[snapshots[i].epoch] = snapshots[i].timeStamp + ptr = i + numSnapshotsProtected++ + } + } + + if numSnapshotsProtected >= maxDataPoints { + break + } + } + return ptr, rv +} + +// getProtectedSnapshots aims to fetch the epochs keep based on a timestamp basis. +// It tries to get NumSnapshotsToKeep snapshots, each of which are separated +// by a time duration of RollbackSamplingInterval. +func getProtectedSnapshots(rollbackSamplingInterval time.Duration, + numSnapshotsToKeep int, + persistedSnapshots []*snapshotMetaData, +) map[uint64]time.Time { + // keep numSnapshotsToKeep - 1 worth of time series snapshots, because we always + // must preserve the very latest snapshot in bolt as well to avoid accidental + // deletes of bolt entries and cleanups by the purger code. + lastPoint, protectedEpochs := getTimeSeriesSnapshots(numSnapshotsToKeep-1, + rollbackSamplingInterval, persistedSnapshots) + if len(protectedEpochs) < numSnapshotsToKeep { + numSnapshotsNeeded := numSnapshotsToKeep - len(protectedEpochs) + // we protected the contiguous snapshots from the last point in time series + for i := 0; i < numSnapshotsNeeded && i < lastPoint; i++ { + protectedEpochs[persistedSnapshots[i].epoch] = persistedSnapshots[i].timeStamp + } + } + + return protectedEpochs +} + +func newCheckPoints(snapshots map[uint64]time.Time) []*snapshotMetaData { + rv := make([]*snapshotMetaData, 0) + + keys := make([]uint64, 0, len(snapshots)) + for k := range snapshots { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return snapshots[keys[i]].Sub(snapshots[keys[j]]) > 0 + }) + + for _, key := range keys { + rv = append(rv, &snapshotMetaData{ + epoch: key, + timeStamp: snapshots[key], + }) + } + + return rv +} + +// Removes enough snapshots from the rootBolt so that the +// s.eligibleForRemoval stays under the NumSnapshotsToKeep policy. +func (s *Scorch) removeOldBoltSnapshots() (numRemoved int, err error) { + persistedSnapshots, err := s.rootBoltSnapshotMetaData() + if err != nil { + return 0, err + } + + if len(persistedSnapshots) <= s.numSnapshotsToKeep { + // we need to keep everything + return 0, nil + } + + protectedSnapshots := getProtectedSnapshots(s.rollbackSamplingInterval, + s.numSnapshotsToKeep, persistedSnapshots) + + var epochsToRemove []uint64 + var newEligible []uint64 + s.rootLock.Lock() + for _, epoch := range s.eligibleForRemoval { + if _, ok := protectedSnapshots[epoch]; ok { + // protected + newEligible = append(newEligible, epoch) + } else { + epochsToRemove = append(epochsToRemove, epoch) + } + } + s.eligibleForRemoval = newEligible + s.rootLock.Unlock() + s.checkPoints = newCheckPoints(protectedSnapshots) + + if len(epochsToRemove) == 0 { + return 0, nil + } + + tx, err := s.rootBolt.Begin(true) + if err != nil { + return 0, err + } + defer func() { + if err == nil { + err = tx.Commit() + } else { + _ = tx.Rollback() + } + if err == nil { + err = s.rootBolt.Sync() + } + }() + + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return 0, nil + } + + for _, epochToRemove := range epochsToRemove { + k := encodeUvarintAscending(nil, epochToRemove) + err = snapshots.DeleteBucket(k) + if err == bolt.ErrBucketNotFound { + err = nil + } + if err == nil { + numRemoved++ + } + } + + return numRemoved, err +} + +func (s *Scorch) maxSegmentIDOnDisk() (uint64, error) { + files, err := os.ReadDir(s.path) + if err != nil { + return 0, err + } + + var rv uint64 + for _, f := range files { + fname := f.Name() + if filepath.Ext(fname) == ".zap" { + prefix := strings.TrimSuffix(fname, ".zap") + id, err2 := strconv.ParseUint(prefix, 16, 64) + if err2 != nil { + return 0, err2 + } + if id > rv { + rv = id + } + } + } + return rv, err +} + +// Removes any *.zap files which aren't listed in the rootBolt. +func (s *Scorch) removeOldZapFiles() error { + liveFileNames, err := s.loadZapFileNames() + if err != nil { + return err + } + + files, err := os.ReadDir(s.path) + if err != nil { + return err + } + + s.rootLock.RLock() + + for _, f := range files { + fname := f.Name() + if filepath.Ext(fname) == ".zap" { + if _, exists := liveFileNames[fname]; !exists && !s.ineligibleForRemoval[fname] && (s.copyScheduled[fname] <= 0) { + err := os.Remove(s.path + string(os.PathSeparator) + fname) + if err != nil { + log.Printf("got err removing file: %s, err: %v", fname, err) + } + } + } + } + + s.rootLock.RUnlock() + + return nil +} + +// In sparse mutation scenario, it can so happen that all protected +// snapshots are older than the numSnapshotsToKeep * rollbackSamplingInterval +// duration. This results in all of them being purged from the boltDB +// and the next iteration of the removeOldData() would end up protecting +// latest contiguous snapshot which is a poor pattern in the rollback checkpoints. +// Hence we try to retain atmost retentionFactor portion worth of old snapshots +// in such a scenario using the following function +func getBoundaryCheckPoint(retentionFactor float64, + checkPoints []*snapshotMetaData, timeStamp time.Time, +) time.Time { + if checkPoints != nil { + boundary := checkPoints[int(math.Floor(float64(len(checkPoints))* + retentionFactor))] + if timeStamp.Sub(boundary.timeStamp) > 0 { + // return the extended boundary which will dictate the older snapshots + // to be retained + return boundary.timeStamp + } + } + + return timeStamp +} + +type snapshotMetaData struct { + epoch uint64 + timeStamp time.Time +} + +func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) { + var rv []*snapshotMetaData + currTime := time.Now() + // including the very latest snapshot there should be n snapshots, so the + // very last one would be tc - (n-1) * d + // for eg for n = 3 the checkpoints preserved should be tc, tc - d, tc - 2d + expirationDuration := time.Duration(s.numSnapshotsToKeep-1) * s.rollbackSamplingInterval + + err := s.rootBolt.View(func(tx *bolt.Tx) error { + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil + } + sc := snapshots.Cursor() + var found bool + // traversal order - latest -> oldest epoch + for sk, _ := sc.Last(); sk != nil; sk, _ = sc.Prev() { + _, snapshotEpoch, err := decodeUvarintAscending(sk) + if err != nil { + continue + } + + if expirationDuration == 0 { + rv = append(rv, &snapshotMetaData{ + epoch: snapshotEpoch, + }) + continue + } + + snapshot := snapshots.Bucket(sk) + if snapshot == nil { + continue + } + metaBucket := snapshot.Bucket(boltMetaDataKey) + if metaBucket == nil { + continue + } + timeStampBytes := metaBucket.Get(boltMetaDataTimeStamp) + var timeStamp time.Time + err = timeStamp.UnmarshalText(timeStampBytes) + if err != nil { + continue + } + // Don't keep snapshots older than + // expiration duration (numSnapshotsToKeep * + // rollbackSamplingInterval, by default) + if currTime.Sub(timeStamp) <= expirationDuration { + rv = append(rv, &snapshotMetaData{ + epoch: snapshotEpoch, + timeStamp: timeStamp, + }) + } else { + if !found { + found = true + boundary := getBoundaryCheckPoint(s.rollbackRetentionFactor, + s.checkPoints, timeStamp) + expirationDuration = currTime.Sub(boundary) + continue + } + k := encodeUvarintAscending(nil, snapshotEpoch) + err = snapshots.DeleteBucket(k) + if err == bolt.ErrBucketNotFound { + err = nil + } + } + } + return nil + }) + return rv, err +} + +func (s *Scorch) RootBoltSnapshotEpochs() ([]uint64, error) { + var rv []uint64 + err := s.rootBolt.View(func(tx *bolt.Tx) error { + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil + } + sc := snapshots.Cursor() + for sk, _ := sc.Last(); sk != nil; sk, _ = sc.Prev() { + _, snapshotEpoch, err := decodeUvarintAscending(sk) + if err != nil { + continue + } + rv = append(rv, snapshotEpoch) + } + return nil + }) + return rv, err +} + +// Returns the *.zap file names that are listed in the rootBolt. +func (s *Scorch) loadZapFileNames() (map[string]struct{}, error) { + rv := map[string]struct{}{} + err := s.rootBolt.View(func(tx *bolt.Tx) error { + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil + } + sc := snapshots.Cursor() + for sk, _ := sc.First(); sk != nil; sk, _ = sc.Next() { + snapshot := snapshots.Bucket(sk) + if snapshot == nil { + continue + } + segc := snapshot.Cursor() + for segk, _ := segc.First(); segk != nil; segk, _ = segc.Next() { + if segk[0] == boltInternalKey[0] { + continue + } + segmentBucket := snapshot.Bucket(segk) + if segmentBucket == nil { + continue + } + pathBytes := segmentBucket.Get(boltPathKey) + if pathBytes == nil { + continue + } + pathString := string(pathBytes) + rv[string(pathString)] = struct{}{} + } + } + return nil + }) + + return rv, err +} diff --git a/index/scorch/reader_test.go b/index/scorch/reader_test.go new file mode 100644 index 0000000..a523334 --- /dev/null +++ b/index/scorch/reader_test.go @@ -0,0 +1,697 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "context" + "encoding/binary" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +func TestIndexReader(t *testing.T) { + cfg := CreateConfig("TestIndexReader") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextFieldWithAnalyzer("name", []uint64{}, []byte("test test test"), testAnalyzer)) + doc.AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("eat more rice"), index.IndexField|index.IncludeTermVectors, testAnalyzer)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + // first look for a term that doesn't exist + reader, err := indexReader.TermFieldReader(context.TODO(), []byte("nope"), "name", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + count := reader.Count() + if count != 0 { + t.Errorf("Expected doc count to be: %d got: %d", 0, count) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + reader, err = indexReader.TermFieldReader(context.TODO(), []byte("test"), "name", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + + count = reader.Count() + if count != expectedCount { + t.Errorf("Expected doc count to be: %d got: %d", expectedCount, count) + } + + var match *index.TermFieldDoc + var actualCount uint64 + match, err = reader.Next(nil) + for err == nil && match != nil { + match, err = reader.Next(nil) + if err != nil { + t.Errorf("unexpected error reading next") + } + actualCount++ + } + if actualCount != count { + t.Errorf("count was 2, but only saw %d", actualCount) + } + + internalIDBogus, err := indexReader.InternalID("a-bogus-docId") + if err != nil { + t.Fatal(err) + } + if internalIDBogus != nil { + t.Errorf("expected bogus docId to have nil InternalID") + } + + internalID2, err := indexReader.InternalID("2") + if err != nil { + t.Fatal(err) + } + expectedMatch := &index.TermFieldDoc{ + ID: internalID2, + Freq: 1, + Norm: 0.5773502588272095, + Vectors: []*index.TermFieldVector{ + { + Field: "desc", + Pos: 3, + Start: 9, + End: 13, + }, + }, + } + tfr, err := indexReader.TermFieldReader(context.TODO(), []byte("rice"), "desc", true, true, true) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + match, err = tfr.Next(nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(expectedMatch, match) { + t.Errorf("got %#v, expected %#v", match, expectedMatch) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now test usage of advance + reader, err = indexReader.TermFieldReader(context.TODO(), []byte("test"), "name", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + + match, err = reader.Advance(internalID2, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match == nil { + t.Fatalf("Expected match, got nil") + } + if !match.ID.Equals(internalID2) { + t.Errorf("Expected ID '2', got '%s'", match.ID) + } + // have to manually construct bogus id, because it doesn't exist + internalID3 := make([]byte, 8) + binary.BigEndian.PutUint64(internalID3, 3) + match, err = reader.Advance(index.IndexInternalID(internalID3), nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match != nil { + t.Errorf("expected nil, got %v", match) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now test creating a reader for a field that doesn't exist + reader, err = indexReader.TermFieldReader(context.TODO(), []byte("water"), "doesnotexist", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + count = reader.Count() + if count != 0 { + t.Errorf("expected count 0 for reader of non-existent field") + } + match, err = reader.Next(nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match != nil { + t.Errorf("expected nil, got %v", match) + } + match, err = reader.Advance(index.IndexInternalID("anywhere"), nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match != nil { + t.Errorf("expected nil, got %v", match) + } +} + +func TestIndexDocIdReader(t *testing.T) { + cfg := CreateConfig("TestIndexDocIdReader") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test test test"))) + doc.AddField(document.NewTextFieldWithIndexingOptions("desc", []uint64{}, []byte("eat more rice"), index.IndexField|index.IncludeTermVectors)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Error(err) + } + }() + + // first get all doc ids + reader, err := indexReader.DocIDReaderAll() + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader.Close() + if err != nil { + t.Fatal(err) + } + }() + + id, err := reader.Next() + if err != nil { + t.Fatal(err) + } + + count := uint64(0) + for id != nil { + count++ + id, err = reader.Next() + if err != nil { + t.Fatal(err) + } + } + if count != expectedCount { + t.Errorf("expected %d, got %d", expectedCount, count) + } + + // try it again, but jump to the second doc this time + reader2, err := indexReader.DocIDReaderAll() + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader2.Close() + if err != nil { + t.Error(err) + } + }() + + internalID2, err := indexReader.InternalID("2") + if err != nil { + t.Fatal(err) + } + + id, err = reader2.Advance(internalID2) + if err != nil { + t.Error(err) + } + if !id.Equals(internalID2) { + t.Errorf("expected to find id '2', got '%s'", id) + } + + // again 3 doesn't exist cannot use internal id for 3 as there is none + // the important aspect is that this id doesn't exist, so its ok + id, err = reader2.Advance(index.IndexInternalID("3")) + if err != nil { + t.Error(err) + } + if id != nil { + t.Errorf("expected to find id '', got '%s'", id) + } +} + +func TestIndexDocIdOnlyReader(t *testing.T) { + cfg := CreateConfig("TestIndexDocIdOnlyReader") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("3") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("5") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("7") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("9") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Error(err) + } + }() + + onlyIds := []string{"1", "5", "9"} + reader, err := indexReader.DocIDReaderOnly(onlyIds) + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader.Close() + if err != nil { + t.Fatal(err) + } + }() + + id, err := reader.Next() + if err != nil { + t.Fatal(err) + } + + count := uint64(0) + for id != nil { + count++ + id, err = reader.Next() + if err != nil { + t.Fatal(err) + } + } + if count != 3 { + t.Errorf("expected 3, got %d", count) + } + + // commented out because advance works with internal ids + // this test presumes we see items in external doc id order + // which is no longer the case, so simply converting external ids + // to internal ones is not logically correct + // not removing though because we need some way to test Advance() + + // // try it again, but jump + // reader2, err := indexReader.DocIDReaderOnly(onlyIds) + // if err != nil { + // t.Errorf("Error accessing doc id reader: %v", err) + // } + // defer func() { + // err := reader2.Close() + // if err != nil { + // t.Error(err) + // } + // }() + // + // id, err = reader2.Advance(index.IndexInternalID("5")) + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("5")) { + // t.Errorf("expected to find id '5', got '%s'", id) + // } + // + // id, err = reader2.Advance(index.IndexInternalID("a")) + // if err != nil { + // t.Error(err) + // } + // if id != nil { + // t.Errorf("expected to find id '', got '%s'", id) + // } + + // some keys aren't actually there + onlyIds = []string{"0", "2", "4", "5", "6", "8", "a"} + reader3, err := indexReader.DocIDReaderOnly(onlyIds) + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader3.Close() + if err != nil { + t.Error(err) + } + }() + + id, err = reader3.Next() + if err != nil { + t.Fatal(err) + } + + count = uint64(0) + for id != nil { + count++ + id, err = reader3.Next() + if err != nil { + t.Fatal(err) + } + } + if count != 1 { + t.Errorf("expected 1, got %d", count) + } + + // commented out because advance works with internal ids + // this test presumes we see items in external doc id order + // which is no longer the case, so simply converting external ids + // to internal ones is not logically correct + // not removing though because we need some way to test Advance() + + // // mix advance and next + // onlyIds = []string{"0", "1", "3", "5", "6", "9"} + // reader4, err := indexReader.DocIDReaderOnly(onlyIds) + // if err != nil { + // t.Errorf("Error accessing doc id reader: %v", err) + // } + // defer func() { + // err := reader4.Close() + // if err != nil { + // t.Error(err) + // } + // }() + // + // // first key is "1" + // id, err = reader4.Next() + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("1")) { + // t.Errorf("expected to find id '1', got '%s'", id) + // } + // + // // advancing to key we dont have gives next + // id, err = reader4.Advance(index.IndexInternalID("2")) + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("3")) { + // t.Errorf("expected to find id '3', got '%s'", id) + // } + // + // // next after advance works + // id, err = reader4.Next() + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("5")) { + // t.Errorf("expected to find id '5', got '%s'", id) + // } + // + // // advancing to key we do have works + // id, err = reader4.Advance(index.IndexInternalID("9")) + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("9")) { + // t.Errorf("expected to find id '9', got '%s'", id) + // } + // + // // advance backwards at end + // id, err = reader4.Advance(index.IndexInternalID("4")) + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("5")) { + // t.Errorf("expected to find id '5', got '%s'", id) + // } + // + // // next after advance works + // id, err = reader4.Next() + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("9")) { + // t.Errorf("expected to find id '9', got '%s'", id) + // } + // + // // advance backwards to key that exists, but not in only set + // id, err = reader4.Advance(index.IndexInternalID("7")) + // if err != nil { + // t.Error(err) + // } + // if !id.Equals(index.IndexInternalID("9")) { + // t.Errorf("expected to find id '9', got '%s'", id) + // } +} + +func TestSegmentIndexAndLocalDocNumFromGlobal(t *testing.T) { + tests := []struct { + offsets []uint64 + globalDocNum uint64 + segmentIndex int + localDocNum uint64 + }{ + // just 1 segment + { + offsets: []uint64{0}, + globalDocNum: 0, + segmentIndex: 0, + localDocNum: 0, + }, + { + offsets: []uint64{0}, + globalDocNum: 1, + segmentIndex: 0, + localDocNum: 1, + }, + { + offsets: []uint64{0}, + globalDocNum: 25, + segmentIndex: 0, + localDocNum: 25, + }, + // now 2 segments, 30 docs in first + { + offsets: []uint64{0, 30}, + globalDocNum: 0, + segmentIndex: 0, + localDocNum: 0, + }, + { + offsets: []uint64{0, 30}, + globalDocNum: 1, + segmentIndex: 0, + localDocNum: 1, + }, + { + offsets: []uint64{0, 30}, + globalDocNum: 25, + segmentIndex: 0, + localDocNum: 25, + }, + { + offsets: []uint64{0, 30}, + globalDocNum: 30, + segmentIndex: 1, + localDocNum: 0, + }, + { + offsets: []uint64{0, 30}, + globalDocNum: 35, + segmentIndex: 1, + localDocNum: 5, + }, + // lots of segments + { + offsets: []uint64{0, 30, 40, 70, 99, 172, 800, 25000}, + globalDocNum: 0, + segmentIndex: 0, + localDocNum: 0, + }, + { + offsets: []uint64{0, 30, 40, 70, 99, 172, 800, 25000}, + globalDocNum: 25, + segmentIndex: 0, + localDocNum: 25, + }, + { + offsets: []uint64{0, 30, 40, 70, 99, 172, 800, 25000}, + globalDocNum: 35, + segmentIndex: 1, + localDocNum: 5, + }, + { + offsets: []uint64{0, 30, 40, 70, 99, 172, 800, 25000}, + globalDocNum: 100, + segmentIndex: 4, + localDocNum: 1, + }, + { + offsets: []uint64{0, 30, 40, 70, 99, 172, 800, 25000}, + globalDocNum: 825, + segmentIndex: 6, + localDocNum: 25, + }, + } + + for _, test := range tests { + i := &IndexSnapshot{ + offsets: test.offsets, + refs: 1, + } + gotSegmentIndex, gotLocalDocNum := i.segmentIndexAndLocalDocNumFromGlobal(test.globalDocNum) + if gotSegmentIndex != test.segmentIndex { + t.Errorf("got segment index %d expected %d for offsets %v globalDocNum %d", gotSegmentIndex, test.segmentIndex, test.offsets, test.globalDocNum) + } + if gotLocalDocNum != test.localDocNum { + t.Errorf("got localDocNum %d expected %d for offsets %v globalDocNum %d", gotLocalDocNum, test.localDocNum, test.offsets, test.globalDocNum) + } + err := i.DecRef() + if err != nil { + t.Errorf("expected no err, got: %v", err) + } + } +} diff --git a/index/scorch/regexp.go b/index/scorch/regexp.go new file mode 100644 index 0000000..5a3584f --- /dev/null +++ b/index/scorch/regexp.go @@ -0,0 +1,63 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "regexp/syntax" + + "github.com/blevesearch/vellum/regexp" +) + +func parseRegexp(pattern string) (a *regexp.Regexp, prefixBeg, prefixEnd []byte, err error) { + // TODO: potential optimization where syntax.Regexp supports a Simplify() API? + + parsed, err := syntax.Parse(pattern, syntax.Perl) + if err != nil { + return nil, nil, nil, err + } + + re, err := regexp.NewParsedWithLimit(pattern, parsed, regexp.DefaultLimit) + if err != nil { + return nil, nil, nil, err + } + + prefix := literalPrefix(parsed) + if prefix != "" { + prefixBeg := []byte(prefix) + prefixEnd := calculateExclusiveEndFromPrefix(prefixBeg) + return re, prefixBeg, prefixEnd, nil + } + + return re, nil, nil, nil +} + +// Returns the literal prefix given the parse tree for a regexp +func literalPrefix(s *syntax.Regexp) string { + // traverse the left-most branch in the parse tree as long as the + // node represents a concatenation + for s != nil && s.Op == syntax.OpConcat { + if len(s.Sub) < 1 { + return "" + } + + s = s.Sub[0] + } + + if s.Op == syntax.OpLiteral && (s.Flags&syntax.FoldCase == 0) { + return string(s.Rune) + } + + return "" // no literal prefix +} diff --git a/index/scorch/regexp_test.go b/index/scorch/regexp_test.go new file mode 100644 index 0000000..305b20c --- /dev/null +++ b/index/scorch/regexp_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "regexp/syntax" + "testing" +) + +func TestLiteralPrefix(t *testing.T) { + tests := []struct { + input, expected string + }{ + {"", ""}, + {"hello", "hello"}, + {"hello.?", "hello"}, + {"hello$", "hello"}, + {`[h][e][l][l][o].*world`, "hello"}, + {`[h-h][e-e][l-l][l-l][o-o].*world`, "hello"}, + {".*", ""}, + {"h.*", "h"}, + {"h.?", "h"}, + {"h[a-z]", "h"}, + {`h\s`, "h"}, + {`(hello)world`, ""}, + {`日本語`, "日本語"}, + {`日本語\w`, "日本語"}, + {`^hello`, ""}, + {`^`, ""}, + {`$`, ""}, + {`(?i)mArTy`, ""}, + } + + for i, test := range tests { + s, err := syntax.Parse(test.input, syntax.Perl) + if err != nil { + t.Fatalf("expected no syntax.Parse error, got: %v", err) + } + + got := literalPrefix(s) + if test.expected != got { + t.Fatalf("test: %d, %+v, got: %s", i, test, got) + } + } +} diff --git a/index/scorch/rollback.go b/index/scorch/rollback.go new file mode 100644 index 0000000..895f939 --- /dev/null +++ b/index/scorch/rollback.go @@ -0,0 +1,216 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "log" + "os" + + bolt "go.etcd.io/bbolt" +) + +type RollbackPoint struct { + epoch uint64 + meta map[string][]byte +} + +func (r *RollbackPoint) GetInternal(key []byte) []byte { + return r.meta[string(key)] +} + +// RollbackPoints returns an array of rollback points available for +// the application to rollback to, with more recent rollback points +// (higher epochs) coming first. +func RollbackPoints(path string) ([]*RollbackPoint, error) { + if len(path) == 0 { + return nil, fmt.Errorf("RollbackPoints: invalid path") + } + + rootBoltPath := path + string(os.PathSeparator) + "root.bolt" + rootBoltOpt := &bolt.Options{ + ReadOnly: true, + } + rootBolt, err := bolt.Open(rootBoltPath, 0600, rootBoltOpt) + if err != nil || rootBolt == nil { + return nil, err + } + + // start a read-only bolt transaction + tx, err := rootBolt.Begin(false) + if err != nil { + return nil, fmt.Errorf("RollbackPoints: failed to start" + + " read-only transaction") + } + + // read-only bolt transactions to be rolled back + defer func() { + _ = tx.Rollback() + _ = rootBolt.Close() + }() + + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil, nil + } + + rollbackPoints := []*RollbackPoint{} + + c1 := snapshots.Cursor() + for k, _ := c1.Last(); k != nil; k, _ = c1.Prev() { + _, snapshotEpoch, err := decodeUvarintAscending(k) + if err != nil { + log.Printf("RollbackPoints:"+ + " unable to parse segment epoch %x, continuing", k) + continue + } + + snapshot := snapshots.Bucket(k) + if snapshot == nil { + log.Printf("RollbackPoints:"+ + " snapshot key, but bucket missing %x, continuing", k) + continue + } + + meta := map[string][]byte{} + c2 := snapshot.Cursor() + for j, _ := c2.First(); j != nil; j, _ = c2.Next() { + if j[0] == boltInternalKey[0] { + internalBucket := snapshot.Bucket(j) + if internalBucket == nil { + err = fmt.Errorf("internal bucket missing") + break + } + err = internalBucket.ForEach(func(key []byte, val []byte) error { + copiedVal := append([]byte(nil), val...) + meta[string(key)] = copiedVal + return nil + }) + if err != nil { + break + } + } + } + + if err != nil { + log.Printf("RollbackPoints:"+ + " failed in fetching internal data: %v", err) + continue + } + + rollbackPoints = append(rollbackPoints, &RollbackPoint{ + epoch: snapshotEpoch, + meta: meta, + }) + } + + return rollbackPoints, nil +} + +// Rollback atomically and durably brings the store back to the point +// in time as represented by the RollbackPoint. +// Rollback() should only be passed a RollbackPoint that came from the +// same store using the RollbackPoints() API along with the index path. +func Rollback(path string, to *RollbackPoint) error { + if to == nil { + return fmt.Errorf("Rollback: RollbackPoint is nil") + } + if len(path) == 0 { + return fmt.Errorf("Rollback: index path is empty") + } + + rootBoltPath := path + string(os.PathSeparator) + "root.bolt" + rootBoltOpt := &bolt.Options{ + ReadOnly: false, + } + rootBolt, err := bolt.Open(rootBoltPath, 0600, rootBoltOpt) + if err != nil || rootBolt == nil { + return err + } + defer func() { + err1 := rootBolt.Close() + if err1 != nil && err == nil { + err = err1 + } + }() + + // pick all the younger persisted epochs in bolt store + // including the target one. + var found bool + var eligibleEpochs []uint64 + err = rootBolt.View(func(tx *bolt.Tx) error { + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil + } + sc := snapshots.Cursor() + for sk, _ := sc.Last(); sk != nil && !found; sk, _ = sc.Prev() { + _, snapshotEpoch, err := decodeUvarintAscending(sk) + if err != nil { + continue + } + if snapshotEpoch == to.epoch { + found = true + } + eligibleEpochs = append(eligibleEpochs, snapshotEpoch) + } + return nil + }) + + if len(eligibleEpochs) == 0 { + return fmt.Errorf("Rollback: no persisted epochs found in bolt") + } + if !found { + return fmt.Errorf("Rollback: target epoch %d not found in bolt", to.epoch) + } + + // start a write transaction + tx, err := rootBolt.Begin(true) + if err != nil { + return err + } + + defer func() { + if err == nil { + err = tx.Commit() + } else { + _ = tx.Rollback() + } + if err == nil { + err = rootBolt.Sync() + } + }() + + snapshots := tx.Bucket(boltSnapshotsBucket) + if snapshots == nil { + return nil + } + for _, epoch := range eligibleEpochs { + k := encodeUvarintAscending(nil, epoch) + if err != nil { + continue + } + if epoch == to.epoch { + // return here as it already processed until the given epoch + return nil + } + err = snapshots.DeleteBucket(k) + if err == bolt.ErrBucketNotFound { + err = nil + } + } + + return err +} diff --git a/index/scorch/rollback_test.go b/index/scorch/rollback_test.go new file mode 100644 index 0000000..4b5406e --- /dev/null +++ b/index/scorch/rollback_test.go @@ -0,0 +1,623 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +func TestIndexRollback(t *testing.T) { + cfg := CreateConfig("TestIndexRollback") + numSnapshotsToKeepOrig := NumSnapshotsToKeep + NumSnapshotsToKeep = 1000 + + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + NumSnapshotsToKeep = numSnapshotsToKeepOrig + + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + + _, ok := idx.(*Scorch) + if !ok { + t.Fatalf("Not a scorch index?") + } + + indexPath, _ := cfg["path"].(string) + // should have no rollback points initially + rollbackPoints, err := RollbackPoints(indexPath) + if err == nil { + t.Fatalf("expected no err, got: %v, %d", err, len(rollbackPoints)) + } + if len(rollbackPoints) != 0 { + t.Fatalf("expected no rollbackPoints, got %d", len(rollbackPoints)) + } + + err = idx.Open() + if err != nil { + t.Fatal(err) + } + // create a batch, insert 2 new documents + batch := index.NewBatch() + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test1"))) + batch.Update(doc) + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2"))) + batch.Update(doc) + + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + readerSlow, err := idx.Reader() // keep snapshot around so it's not cleaned up + if err != nil { + t.Fatal(err) + } + defer func() { + _ = readerSlow.Close() + }() + + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + // fetch rollback points after first batch + rollbackPoints, err = RollbackPoints(indexPath) + if err != nil { + t.Fatalf("expected no err, got: %v, %d", err, len(rollbackPoints)) + } + if len(rollbackPoints) == 0 { + t.Fatalf("expected some rollbackPoints, got none") + } + + // set this as a rollback point for the future + rollbackPoint := rollbackPoints[0] + + err = idx.Open() + if err != nil { + t.Fatal(err) + } + // create another batch, insert 2 new documents, and delete an existing one + batch = index.NewBatch() + doc = document.NewDocument("3") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test3"))) + batch.Update(doc) + doc = document.NewDocument("4") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test4"))) + batch.Update(doc) + batch.Delete("1") + + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + rollbackPointsB, err := RollbackPoints(indexPath) + if err != nil || len(rollbackPointsB) <= len(rollbackPoints) { + t.Fatalf("expected no err, got: %v, %d", err, len(rollbackPointsB)) + } + + found := false + for _, p := range rollbackPointsB { + if rollbackPoint.epoch == p.epoch { + found = true + } + } + if !found { + t.Fatalf("expected rollbackPoint epoch to still be available") + } + + err = idx.Open() + if err != nil { + t.Fatal(err) + } + + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + + docCount, err := reader.DocCount() + if err != nil { + t.Fatal(err) + } + + // expect docs 2, 3, 4 + if docCount != 3 { + t.Fatalf("unexpected doc count: %v", docCount) + } + ret, err := reader.Document("1") + if err != nil || ret != nil { + t.Fatal(ret, err) + } + ret, err = reader.Document("2") + if err != nil || ret == nil { + t.Fatal(ret, err) + } + ret, err = reader.Document("3") + if err != nil || ret == nil { + t.Fatal(ret, err) + } + ret, err = reader.Document("4") + if err != nil || ret == nil { + t.Fatal(ret, err) + } + + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + // rollback to a non existing rollback point + err = Rollback(indexPath, &RollbackPoint{epoch: 100}) + if err == nil { + t.Fatalf("expected err: Rollback: target epoch 100 not found in bolt") + } + + // rollback to the selected rollback point + err = Rollback(indexPath, rollbackPoint) + if err != nil { + t.Fatal(err) + } + + err = idx.Open() + if err != nil { + t.Fatal(err) + } + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + + docCount, err = reader.DocCount() + if err != nil { + t.Fatal(err) + } + + // expect only docs 1, 2 + if docCount != 2 { + t.Fatalf("unexpected doc count: %v", docCount) + } + ret, err = reader.Document("1") + if err != nil || ret == nil { + t.Fatal(ret, err) + } + ret, err = reader.Document("2") + if err != nil || ret == nil { + t.Fatal(ret, err) + } + ret, err = reader.Document("3") + if err != nil || ret != nil { + t.Fatal(ret, err) + } + ret, err = reader.Document("4") + if err != nil || ret != nil { + t.Fatal(ret, err) + } + + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestGetProtectedSnapshots(t *testing.T) { + origRollbackSamplingInterval := RollbackSamplingInterval + defer func() { + RollbackSamplingInterval = origRollbackSamplingInterval + }() + RollbackSamplingInterval = 10 * time.Minute + currentTimeStamp := time.Now() + tests := []struct { + title string + metaData []*snapshotMetaData + numSnapshotsToKeep int + expCount int + expEpochs []uint64 + }{ + { + title: "epochs that have exact timestamps as per expectation for protecting", + metaData: []*snapshotMetaData{ + {epoch: 100, timeStamp: currentTimeStamp}, + {epoch: 99, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 12))}, + {epoch: 88, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 6))}, + {epoch: 50, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval))}, + {epoch: 35, timeStamp: currentTimeStamp.Add(-(6 * RollbackSamplingInterval / 5))}, + {epoch: 10, timeStamp: currentTimeStamp.Add(-(2 * RollbackSamplingInterval))}, + }, + numSnapshotsToKeep: 3, + expCount: 3, + expEpochs: []uint64{100, 50, 10}, + }, + { + title: "epochs that have exact timestamps as per expectation for protecting", + metaData: []*snapshotMetaData{ + {epoch: 100, timeStamp: currentTimeStamp}, + {epoch: 99, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 12))}, + {epoch: 88, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 6))}, + {epoch: 50, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval))}, + }, + numSnapshotsToKeep: 2, + expCount: 2, + expEpochs: []uint64{100, 50}, + }, + { + title: "epochs that have timestamps approximated to the expected value, " + + "always retain the latest one", + metaData: []*snapshotMetaData{ + {epoch: 100, timeStamp: currentTimeStamp}, + {epoch: 99, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 12))}, + {epoch: 88, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 6))}, + {epoch: 50, timeStamp: currentTimeStamp.Add(-(3 * RollbackSamplingInterval / 4))}, + {epoch: 35, timeStamp: currentTimeStamp.Add(-(6 * RollbackSamplingInterval / 5))}, + {epoch: 10, timeStamp: currentTimeStamp.Add(-(2 * RollbackSamplingInterval))}, + }, + numSnapshotsToKeep: 3, + expCount: 3, + expEpochs: []uint64{100, 35, 10}, + }, + { + title: "protecting epochs when we don't have enough snapshots with RollbackSamplingInterval" + + " separated timestamps", + metaData: []*snapshotMetaData{ + {epoch: 100, timeStamp: currentTimeStamp}, + {epoch: 99, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 12))}, + {epoch: 88, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 6))}, + {epoch: 50, timeStamp: currentTimeStamp.Add(-(3 * RollbackSamplingInterval / 4))}, + {epoch: 35, timeStamp: currentTimeStamp.Add(-(5 * RollbackSamplingInterval / 6))}, + {epoch: 10, timeStamp: currentTimeStamp.Add(-(7 * RollbackSamplingInterval / 8))}, + }, + numSnapshotsToKeep: 4, + expCount: 4, + expEpochs: []uint64{100, 99, 88, 10}, + }, + { + title: "epochs of which some are approximated to the expected timestamps, and" + + " we don't have enough snapshots with RollbackSamplingInterval separated timestamps", + metaData: []*snapshotMetaData{ + {epoch: 100, timeStamp: currentTimeStamp}, + {epoch: 99, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 12))}, + {epoch: 88, timeStamp: currentTimeStamp.Add(-(RollbackSamplingInterval / 6))}, + {epoch: 50, timeStamp: currentTimeStamp.Add(-(3 * RollbackSamplingInterval / 4))}, + {epoch: 35, timeStamp: currentTimeStamp.Add(-(8 * RollbackSamplingInterval / 7))}, + {epoch: 10, timeStamp: currentTimeStamp.Add(-(6 * RollbackSamplingInterval / 5))}, + }, + numSnapshotsToKeep: 3, + expCount: 3, + expEpochs: []uint64{100, 50, 10}, + }, + } + + for i, test := range tests { + protectedEpochs := getProtectedSnapshots(RollbackSamplingInterval, + test.numSnapshotsToKeep, test.metaData) + if len(protectedEpochs) != test.expCount { + t.Errorf("%d test: %s, getProtectedSnapshots expected to return %d "+ + "snapshots, but got: %d", i, test.title, test.expCount, len(protectedEpochs)) + } + for _, e := range test.expEpochs { + if _, found := protectedEpochs[e]; !found { + t.Errorf("%d test: %s, %d epoch expected to be protected, "+ + "but missing from protected list: %v", i, test.title, e, protectedEpochs) + } + } + } +} + +func indexDummyData(t *testing.T, scorchi *Scorch, i int) { + // create a batch, insert 2 new documents + batch := index.NewBatch() + doc := document.NewDocument(fmt.Sprintf("%d", i)) + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test1"))) + batch.Update(doc) + doc = document.NewDocument(fmt.Sprintf("%d", i+1)) + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2"))) + batch.Update(doc) + + err := scorchi.Batch(batch) + if err != nil { + t.Fatal(err) + } +} + +type testFSDirector string + +func (f testFSDirector) GetWriter(filePath string) (io.WriteCloser, + error) { + dir, file := filepath.Split(filePath) + if dir != "" { + err := os.MkdirAll(filepath.Join(string(f), dir), os.ModePerm) + if err != nil { + return nil, err + } + } + + return os.OpenFile(filepath.Join(string(f), dir, file), + os.O_RDWR|os.O_CREATE, 0600) +} + +func TestLatestSnapshotProtected(t *testing.T) { + cfg := CreateConfig("TestLatestSnapshotProtected") + numSnapshotsToKeepOrig := NumSnapshotsToKeep + NumSnapshotsToKeep = 3 + rollbackSamplingIntervalOrig := RollbackSamplingInterval + RollbackSamplingInterval = 10 * time.Second + + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + NumSnapshotsToKeep = numSnapshotsToKeepOrig + RollbackSamplingInterval = rollbackSamplingIntervalOrig + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + // disable merger and purger + RegistryEventCallbacks["test"] = func(e Event) bool { + if e.Kind == EventKindPreMergeCheck || e.Kind == EventKindPurgerCheck { + return false + } + return true + } + cfg["eventCallbackName"] = "test" + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + + scorchi, ok := idx.(*Scorch) + if !ok { + t.Fatalf("Not a scorch index?") + } + + err = scorchi.Open() + if err != nil { + t.Fatal(err) + } + + // replicate the following scenario of persistence of snapshots + // tc, tc - d/12, tc - d/6, tc - 3d/4, tc - 5d/6, tc - 6d/5 + // approximate timestamps where there's a chance that the latest snapshot + // might not fit into the time-series + indexDummyData(t, scorchi, 1) + persistedSnapshots, err := scorchi.rootBoltSnapshotMetaData() + if err != nil { + t.Fatal(err) + } + + if len(persistedSnapshots) != 1 { + t.Fatalf("expected 1 persisted snapshot, got %d", len(persistedSnapshots)) + } + time.Sleep(4 * RollbackSamplingInterval / 5) + indexDummyData(t, scorchi, 3) + time.Sleep(9 * RollbackSamplingInterval / 20) + indexDummyData(t, scorchi, 5) + time.Sleep(7 * RollbackSamplingInterval / 12) + indexDummyData(t, scorchi, 7) + time.Sleep(1 * RollbackSamplingInterval / 12) + indexDummyData(t, scorchi, 9) + + persistedSnapshots, err = scorchi.rootBoltSnapshotMetaData() + if err != nil { + t.Fatal(err) + } + + protectedSnapshots := getProtectedSnapshots(RollbackSamplingInterval, NumSnapshotsToKeep, persistedSnapshots) + if len(protectedSnapshots) != 3 { + t.Fatalf("expected %d protected snapshots, got %d", NumSnapshotsToKeep, len(protectedSnapshots)) + } + if _, ok := protectedSnapshots[persistedSnapshots[0].epoch]; !ok { + t.Fatalf("expected %d to be protected, but not found", persistedSnapshots[0].epoch) + } +} + +func TestBackupRacingWithPurge(t *testing.T) { + cfg := CreateConfig("TestBackupRacingWithPurge") + numSnapshotsToKeepOrig := NumSnapshotsToKeep + NumSnapshotsToKeep = 3 + rollbackSamplingIntervalOrig := RollbackSamplingInterval + RollbackSamplingInterval = 10 * time.Second + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + NumSnapshotsToKeep = numSnapshotsToKeepOrig + RollbackSamplingInterval = rollbackSamplingIntervalOrig + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + // disable merger and purger + RegistryEventCallbacks["test"] = func(e Event) bool { + if e.Kind == EventKindPreMergeCheck || e.Kind == EventKindPurgerCheck { + return false + } + return true + } + cfg["eventCallbackName"] = "test" + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + defer idx.Close() + + scorchi, ok := idx.(*Scorch) + if !ok { + t.Fatalf("Not a scorch index?") + } + + err = scorchi.Open() + if err != nil { + t.Fatal(err) + } + + // replicate the following scenario of persistence of snapshots + // tc, tc - d/12, tc - d/6, tc - 3d/4, tc - 5d/6, tc - 6d/5 + // approximate timestamps where there's a chance that the latest snapshot + // might not fit into the time-series + indexDummyData(t, scorchi, 1) + time.Sleep(4 * RollbackSamplingInterval / 5) + indexDummyData(t, scorchi, 3) + time.Sleep(9 * RollbackSamplingInterval / 20) + indexDummyData(t, scorchi, 5) + time.Sleep(7 * RollbackSamplingInterval / 12) + indexDummyData(t, scorchi, 7) + time.Sleep(1 * RollbackSamplingInterval / 12) + indexDummyData(t, scorchi, 9) + + // now if the purge code is invoked, there's a possiblity of the latest snapshot + // being removed from bolt and the corresponding file segment getting cleaned up. + scorchi.removeOldData() + + copyReader := scorchi.CopyReader() + defer func() { copyReader.CloseCopyReader() }() + + backupidxConfig := CreateConfig("backup-directory") + err = InitTest(backupidxConfig) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(backupidxConfig) + if err != nil { + t.Log(err) + } + }() + + // if the latest snapshot was purged, the following will return error + err = copyReader.CopyTo(testFSDirector(backupidxConfig["path"].(string))) + if err != nil { + t.Fatalf("error copying the index: %v", err) + } +} + +func TestSparseMutationCheckpointing(t *testing.T) { + cfg := CreateConfig("TestSparseMutationCheckpointing") + numSnapshotsToKeepOrig := NumSnapshotsToKeep + NumSnapshotsToKeep = 3 + rollbackSamplingIntervalOrig := RollbackSamplingInterval + RollbackSamplingInterval = 2 * time.Second + + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + NumSnapshotsToKeep = numSnapshotsToKeepOrig + RollbackSamplingInterval = rollbackSamplingIntervalOrig + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + // disable merger and purger + RegistryEventCallbacks["test"] = func(e Event) bool { + if e.Kind == EventKindPreMergeCheck { + return false + } + return true + } + cfg["eventCallbackName"] = "test" + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + + scorchi, ok := idx.(*Scorch) + if !ok { + t.Fatalf("Not a scorch index?") + } + + err = scorchi.Open() + if err != nil { + t.Fatal(err) + } + + // create 4 snapshots every 2 seconds + indexDummyData(t, scorchi, 1) + time.Sleep(RollbackSamplingInterval) + indexDummyData(t, scorchi, 3) + time.Sleep(RollbackSamplingInterval) + indexDummyData(t, scorchi, 5) + time.Sleep(RollbackSamplingInterval) + indexDummyData(t, scorchi, 7) + + // now the another snapshot is persisted outside of the window of checkpointing + // and we should be able to retain some older checkpoints as well along with + // the latest one + time.Sleep(time.Duration(NumSnapshotsToKeep) * RollbackSamplingInterval) + indexDummyData(t, scorchi, 9) + + persistedSnapshots, err := scorchi.rootBoltSnapshotMetaData() + if err != nil { + t.Fatal(err) + } + + // should have more than 1 snapshots + protectedSnapshots := getProtectedSnapshots(RollbackSamplingInterval, NumSnapshotsToKeep, persistedSnapshots) + if len(protectedSnapshots) <= 1 { + t.Fatalf("expected %d protected snapshots, got %d", NumSnapshotsToKeep, len(protectedSnapshots)) + } +} diff --git a/index/scorch/scorch.go b/index/scorch/scorch.go new file mode 100644 index 0000000..54dcb92 --- /dev/null +++ b/index/scorch/scorch.go @@ -0,0 +1,942 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/RoaringBitmap/roaring/v2" + "github.com/blevesearch/bleve/v2/registry" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" + bolt "go.etcd.io/bbolt" +) + +const Name = "scorch" + +const Version uint8 = 2 + +var ErrClosed = fmt.Errorf("scorch closed") + +type Scorch struct { + nextSegmentID uint64 + stats Stats + iStats internalStats + + readOnly bool + version uint8 + config map[string]interface{} + analysisQueue *index.AnalysisQueue + path string + + unsafeBatch bool + + rootLock sync.RWMutex + + root *IndexSnapshot // holds 1 ref-count on the root + rootPersisted []chan error // closed when root is persisted + persistedCallbacks []index.BatchCallback + nextSnapshotEpoch uint64 + eligibleForRemoval []uint64 // Index snapshot epochs that are safe to GC. + ineligibleForRemoval map[string]bool // Filenames that should not be GC'ed yet. + + // keeps track of segments scheduled for online copy/backup operation. Each segment's filename maps to + // the count of copy schedules. Segments with non-zero counts are protected from removal by the cleanup + // operation. Counts decrement upon successful copy, allowing removal of segments with zero or absent counts. + // must be accessed within the rootLock as it is accessed by the asynchronous cleanup routine. + copyScheduled map[string]int + + numSnapshotsToKeep int + rollbackRetentionFactor float64 + checkPoints []*snapshotMetaData + rollbackSamplingInterval time.Duration + closeCh chan struct{} + introductions chan *segmentIntroduction + persists chan *persistIntroduction + merges chan *segmentMerge + introducerNotifier chan *epochWatcher + persisterNotifier chan *epochWatcher + rootBolt *bolt.DB + asyncTasks sync.WaitGroup + + onEvent func(event Event) bool + onAsyncError func(err error, path string) + + forceMergeRequestCh chan *mergerCtrl + + segPlugin SegmentPlugin + + spatialPlugin index.SpatialAnalyzerPlugin +} + +// AsyncPanicError is passed to scorch asyncErrorHandler when panic occurs in scorch background process +type AsyncPanicError struct { + Source string + Path string +} + +func (e *AsyncPanicError) Error() string { + return fmt.Sprintf("%s panic when processing %s", e.Source, e.Path) +} + +type internalStats struct { + persistEpoch uint64 + persistSnapshotSize uint64 + mergeEpoch uint64 + mergeSnapshotSize uint64 + newSegBufBytesAdded uint64 + newSegBufBytesRemoved uint64 + analysisBytesAdded uint64 + analysisBytesRemoved uint64 +} + +func NewScorch(storeName string, + config map[string]interface{}, + analysisQueue *index.AnalysisQueue, +) (index.Index, error) { + rv := &Scorch{ + version: Version, + config: config, + analysisQueue: analysisQueue, + nextSnapshotEpoch: 1, + closeCh: make(chan struct{}), + ineligibleForRemoval: map[string]bool{}, + forceMergeRequestCh: make(chan *mergerCtrl, 1), + segPlugin: defaultSegmentPlugin, + copyScheduled: map[string]int{}, + } + + forcedSegmentType, forcedSegmentVersion, err := configForceSegmentTypeVersion(config) + if err != nil { + return nil, err + } + if forcedSegmentType != "" && forcedSegmentVersion != 0 { + err := rv.loadSegmentPlugin(forcedSegmentType, + uint32(forcedSegmentVersion)) + if err != nil { + return nil, err + } + } + + typ, ok := config["spatialPlugin"].(string) + if ok { + if err := rv.loadSpatialAnalyzerPlugin(typ); err != nil { + return nil, err + } + } + + rv.root = &IndexSnapshot{parent: rv, refs: 1, creator: "NewScorch"} + ro, ok := config["read_only"].(bool) + if ok { + rv.readOnly = ro + } + ub, ok := config["unsafe_batch"].(bool) + if ok { + rv.unsafeBatch = ub + } + ecbName, ok := config["eventCallbackName"].(string) + if ok { + rv.onEvent = RegistryEventCallbacks[ecbName] + } + aecbName, ok := config["asyncErrorCallbackName"].(string) + if ok { + rv.onAsyncError = RegistryAsyncErrorCallbacks[aecbName] + } + // validate any custom persistor options to + // prevent an async error in the persistor routine + _, err = rv.parsePersisterOptions() + if err != nil { + return nil, err + } + // validate any custom merge planner options to + // prevent an async error in the merger routine + _, err = rv.parseMergePlannerOptions() + if err != nil { + return nil, err + } + + return rv, nil +} + +// configForceSegmentTypeVersion checks if the caller has requested a +// specific segment type/version +func configForceSegmentTypeVersion(config map[string]interface{}) (string, uint32, error) { + forcedSegmentVersion, err := parseToInteger(config["forceSegmentVersion"]) + if err != nil { + return "", 0, nil + } + + forcedSegmentType, ok := config["forceSegmentType"].(string) + if !ok { + return "", 0, fmt.Errorf( + "forceSegmentVersion set to %d, must also specify forceSegmentType", forcedSegmentVersion) + } + + return forcedSegmentType, uint32(forcedSegmentVersion), nil +} + +func (s *Scorch) NumEventsBlocking() uint64 { + eventsCompleted := atomic.LoadUint64(&s.stats.TotEventTriggerCompleted) + eventsStarted := atomic.LoadUint64(&s.stats.TotEventTriggerStarted) + return eventsStarted - eventsCompleted +} + +func (s *Scorch) fireEvent(kind EventKind, dur time.Duration) bool { + res := true + if s.onEvent != nil { + atomic.AddUint64(&s.stats.TotEventTriggerStarted, 1) + res = s.onEvent(Event{Kind: kind, Scorch: s, Duration: dur}) + atomic.AddUint64(&s.stats.TotEventTriggerCompleted, 1) + } + return res +} + +func (s *Scorch) fireAsyncError(err error) { + if s.onAsyncError != nil { + s.onAsyncError(err, s.path) + } + atomic.AddUint64(&s.stats.TotOnErrors, 1) +} + +func (s *Scorch) Open() error { + err := s.openBolt() + if err != nil { + return err + } + + s.asyncTasks.Add(1) + go s.introducerLoop() + + if !s.readOnly && s.path != "" { + s.asyncTasks.Add(1) + go s.persisterLoop() + s.asyncTasks.Add(1) + go s.mergerLoop() + } + + return nil +} + +func (s *Scorch) openBolt() error { + var ok bool + s.path, ok = s.config["path"].(string) + if !ok { + return fmt.Errorf("must specify path") + } + if s.path == "" { + s.unsafeBatch = true + } + + rootBoltOpt := *bolt.DefaultOptions + if s.readOnly { + rootBoltOpt.ReadOnly = true + rootBoltOpt.OpenFile = func(path string, flag int, mode os.FileMode) (*os.File, error) { + // Bolt appends an O_CREATE flag regardless. + // See - https://github.com/etcd-io/bbolt/blob/v1.3.5/db.go#L210 + // Use os.O_RDONLY only if path exists (#1623) + if _, err := os.Stat(path); os.IsNotExist(err) { + return os.OpenFile(path, flag, mode) + } + return os.OpenFile(path, os.O_RDONLY, mode) + } + } else { + if s.path != "" { + err := os.MkdirAll(s.path, 0o700) + if err != nil { + return err + } + } + } + + if boltTimeoutStr, ok := s.config["bolt_timeout"].(string); ok { + var err error + boltTimeout, err := time.ParseDuration(boltTimeoutStr) + if err != nil { + return fmt.Errorf("invalid duration specified for bolt_timeout: %v", err) + } + rootBoltOpt.Timeout = boltTimeout + } + + rootBoltPath := s.path + string(os.PathSeparator) + "root.bolt" + var err error + if s.path != "" { + s.rootBolt, err = bolt.Open(rootBoltPath, 0o600, &rootBoltOpt) + if err != nil { + return err + } + + // now see if there is any existing state to load + err = s.loadFromBolt() + if err != nil { + _ = s.Close() + return err + } + } + + atomic.StoreUint64(&s.stats.TotFileSegmentsAtRoot, uint64(len(s.root.segment))) + + s.introductions = make(chan *segmentIntroduction) + s.persists = make(chan *persistIntroduction) + s.merges = make(chan *segmentMerge) + s.introducerNotifier = make(chan *epochWatcher, 1) + s.persisterNotifier = make(chan *epochWatcher, 1) + s.closeCh = make(chan struct{}) + s.forceMergeRequestCh = make(chan *mergerCtrl, 1) + + if !s.readOnly && s.path != "" { + err := s.removeOldZapFiles() // Before persister or merger create any new files. + if err != nil { + _ = s.Close() + return err + } + } + + s.numSnapshotsToKeep = NumSnapshotsToKeep + if v, ok := s.config["numSnapshotsToKeep"]; ok { + var t int + if t, err = parseToInteger(v); err != nil { + return fmt.Errorf("numSnapshotsToKeep parse err: %v", err) + } + if t > 0 { + s.numSnapshotsToKeep = t + } + } + + s.rollbackSamplingInterval = RollbackSamplingInterval + if v, ok := s.config["rollbackSamplingInterval"]; ok { + var t time.Duration + if t, err = parseToTimeDuration(v); err != nil { + return fmt.Errorf("rollbackSamplingInterval parse err: %v", err) + } + s.rollbackSamplingInterval = t + } + + s.rollbackRetentionFactor = RollbackRetentionFactor + if v, ok := s.config["rollbackRetentionFactor"]; ok { + var r float64 + if r, ok = v.(float64); ok { + return fmt.Errorf("rollbackRetentionFactor parse err: %v", err) + } + s.rollbackRetentionFactor = r + } + + typ, ok := s.config["spatialPlugin"].(string) + if ok { + if err := s.loadSpatialAnalyzerPlugin(typ); err != nil { + return err + } + } + + return nil +} + +func (s *Scorch) Close() (err error) { + startTime := time.Now() + defer func() { + s.fireEvent(EventKindClose, time.Since(startTime)) + }() + + s.fireEvent(EventKindCloseStart, 0) + + // signal to async tasks we want to close + close(s.closeCh) + // wait for them to close + s.asyncTasks.Wait() + // now close the root bolt + if s.rootBolt != nil { + err = s.rootBolt.Close() + s.rootLock.Lock() + if s.root != nil { + err2 := s.root.DecRef() + if err == nil { + err = err2 + } + } + s.root = nil + s.rootLock.Unlock() + } + + return +} + +func (s *Scorch) Update(doc index.Document) error { + b := index.NewBatch() + b.Update(doc) + return s.Batch(b) +} + +func (s *Scorch) Delete(id string) error { + b := index.NewBatch() + b.Delete(id) + return s.Batch(b) +} + +// Batch applices a batch of changes to the index atomically +func (s *Scorch) Batch(batch *index.Batch) (err error) { + start := time.Now() + + // notify handlers that we're about to index a batch of data + s.fireEvent(EventKindBatchIntroductionStart, 0) + defer func() { + s.fireEvent(EventKindBatchIntroduction, time.Since(start)) + }() + + resultChan := make(chan index.Document, len(batch.IndexOps)) + + var numUpdates uint64 + var numDeletes uint64 + var numPlainTextBytes uint64 + var ids []string + for docID, doc := range batch.IndexOps { + if doc != nil { + // insert _id field + doc.AddIDField() + numUpdates++ + numPlainTextBytes += doc.NumPlainTextBytes() + } else { + numDeletes++ + } + ids = append(ids, docID) + } + + // FIXME could sort ids list concurrent with analysis? + + if numUpdates > 0 { + go func() { + for k := range batch.IndexOps { + doc := batch.IndexOps[k] + if doc != nil { + // put the work on the queue + s.analysisQueue.Queue(func() { + analyze(doc, s.setSpatialAnalyzerPlugin) + resultChan <- doc + }) + } + } + }() + } + + // wait for analysis result + analysisResults := make([]index.Document, int(numUpdates)) + var itemsDeQueued uint64 + var totalAnalysisSize int + for itemsDeQueued < numUpdates { + result := <-resultChan + resultSize := result.Size() + // check if the document is searchable by the index + if result.Indexed() { + atomic.AddUint64(&s.stats.TotMutationsFiltered, 1) + } + atomic.AddUint64(&s.iStats.analysisBytesAdded, uint64(resultSize)) + totalAnalysisSize += resultSize + analysisResults[itemsDeQueued] = result + itemsDeQueued++ + } + close(resultChan) + defer atomic.AddUint64(&s.iStats.analysisBytesRemoved, uint64(totalAnalysisSize)) + + atomic.AddUint64(&s.stats.TotAnalysisTime, uint64(time.Since(start))) + + indexStart := time.Now() + + var newSegment segment.Segment + var bufBytes uint64 + stats := newFieldStats() + + if len(analysisResults) > 0 { + newSegment, bufBytes, err = s.segPlugin.New(analysisResults) + if err != nil { + return err + } + if segB, ok := newSegment.(segment.DiskStatsReporter); ok { + atomic.AddUint64(&s.stats.TotBytesWrittenAtIndexTime, + segB.BytesWritten()) + } + atomic.AddUint64(&s.iStats.newSegBufBytesAdded, bufBytes) + if fsr, ok := newSegment.(segment.FieldStatsReporter); ok { + fsr.UpdateFieldStats(stats) + } + } else { + atomic.AddUint64(&s.stats.TotBatchesEmpty, 1) + } + + err = s.prepareSegment(newSegment, ids, batch.InternalOps, batch.PersistedCallback(), stats) + if err != nil { + if newSegment != nil { + _ = newSegment.Close() + } + atomic.AddUint64(&s.stats.TotOnErrors, 1) + } else { + atomic.AddUint64(&s.stats.TotUpdates, numUpdates) + atomic.AddUint64(&s.stats.TotDeletes, numDeletes) + atomic.AddUint64(&s.stats.TotBatches, 1) + atomic.AddUint64(&s.stats.TotIndexedPlainTextBytes, numPlainTextBytes) + } + + atomic.AddUint64(&s.iStats.newSegBufBytesRemoved, bufBytes) + atomic.AddUint64(&s.stats.TotIndexTime, uint64(time.Since(indexStart))) + + return err +} + +func (s *Scorch) prepareSegment(newSegment segment.Segment, ids []string, + internalOps map[string][]byte, persistedCallback index.BatchCallback, stats *fieldStats, +) error { + // new introduction + introduction := &segmentIntroduction{ + id: atomic.AddUint64(&s.nextSegmentID, 1), + data: newSegment, + ids: ids, + internal: internalOps, + stats: stats, + applied: make(chan error), + persistedCallback: persistedCallback, + } + + if !s.unsafeBatch { + introduction.persisted = make(chan error, 1) + } + + // optimistically prepare obsoletes outside of rootLock + s.rootLock.RLock() + root := s.root + root.AddRef() + s.rootLock.RUnlock() + + defer func() { _ = root.DecRef() }() + + introduction.obsoletes = make(map[uint64]*roaring.Bitmap, len(root.segment)) + + for _, seg := range root.segment { + delta, err := seg.segment.DocNumbers(ids) + if err != nil { + return err + } + introduction.obsoletes[seg.id] = delta + } + + introStartTime := time.Now() + + s.introductions <- introduction + + // block until this segment is applied + err := <-introduction.applied + if err != nil { + return err + } + + if introduction.persisted != nil { + err = <-introduction.persisted + } + + introTime := uint64(time.Since(introStartTime)) + atomic.AddUint64(&s.stats.TotBatchIntroTime, introTime) + if atomic.LoadUint64(&s.stats.MaxBatchIntroTime) < introTime { + atomic.StoreUint64(&s.stats.MaxBatchIntroTime, introTime) + } + + return err +} + +func (s *Scorch) SetInternal(key, val []byte) error { + b := index.NewBatch() + b.SetInternal(key, val) + return s.Batch(b) +} + +func (s *Scorch) DeleteInternal(key []byte) error { + b := index.NewBatch() + b.DeleteInternal(key) + return s.Batch(b) +} + +// Reader returns a low-level accessor on the index data. Close it to +// release associated resources. +func (s *Scorch) Reader() (index.IndexReader, error) { + return s.currentSnapshot(), nil +} + +func (s *Scorch) currentSnapshot() *IndexSnapshot { + s.rootLock.RLock() + rv := s.root + if rv != nil { + rv.AddRef() + } + s.rootLock.RUnlock() + return rv +} + +func (s *Scorch) Stats() json.Marshaler { + return &s.stats +} + +func (s *Scorch) BytesReadQueryTime() uint64 { + return s.stats.TotBytesReadAtQueryTime +} + +func (s *Scorch) diskFileStats(rootSegmentPaths map[string]struct{}) (uint64, + uint64, uint64, +) { + var numFilesOnDisk, numBytesUsedDisk, numBytesOnDiskByRoot uint64 + if s.path != "" { + files, err := os.ReadDir(s.path) + if err == nil { + for _, f := range files { + if !f.IsDir() { + if finfo, err := f.Info(); err == nil { + numBytesUsedDisk += uint64(finfo.Size()) + numFilesOnDisk++ + if rootSegmentPaths != nil { + fname := s.path + string(os.PathSeparator) + finfo.Name() + if _, fileAtRoot := rootSegmentPaths[fname]; fileAtRoot { + numBytesOnDiskByRoot += uint64(finfo.Size()) + } + } + } + } + } + } + } + // if no root files path given, then consider all disk files. + if rootSegmentPaths == nil { + return numFilesOnDisk, numBytesUsedDisk, numBytesUsedDisk + } + + return numFilesOnDisk, numBytesUsedDisk, numBytesOnDiskByRoot +} + +func (s *Scorch) StatsMap() map[string]interface{} { + m := s.stats.ToMap() + + indexSnapshot := s.currentSnapshot() + if indexSnapshot == nil { + return nil + } + + defer func() { + _ = indexSnapshot.Close() + }() + + rootSegPaths := indexSnapshot.diskSegmentsPaths() + + s.rootLock.RLock() + m["CurFilesIneligibleForRemoval"] = uint64(len(s.ineligibleForRemoval)) + s.rootLock.RUnlock() + + numFilesOnDisk, numBytesUsedDisk, numBytesOnDiskByRoot := s.diskFileStats(rootSegPaths) + + m["CurOnDiskBytes"] = numBytesUsedDisk + m["CurOnDiskFiles"] = numFilesOnDisk + + // TODO: consider one day removing these backwards compatible + // names for apps using the old names + m["updates"] = m["TotUpdates"] + m["deletes"] = m["TotDeletes"] + m["batches"] = m["TotBatches"] + m["errors"] = m["TotOnErrors"] + m["analysis_time"] = m["TotAnalysisTime"] + m["index_time"] = m["TotIndexTime"] + m["term_searchers_started"] = m["TotTermSearchersStarted"] + m["term_searchers_finished"] = m["TotTermSearchersFinished"] + m["knn_searches"] = m["TotKNNSearches"] + m["synonym_searches"] = m["TotSynonymSearches"] + m["total_mutations_filtered"] = m["TotMutationsFiltered"] + + m["num_bytes_read_at_query_time"] = m["TotBytesReadAtQueryTime"] + m["num_plain_text_bytes_indexed"] = m["TotIndexedPlainTextBytes"] + m["num_bytes_written_at_index_time"] = m["TotBytesWrittenAtIndexTime"] + m["num_items_introduced"] = m["TotIntroducedItems"] + m["num_items_persisted"] = m["TotPersistedItems"] + m["num_recs_to_persist"] = m["TotItemsToPersist"] + // total disk bytes found in index directory inclusive of older snapshots + m["num_bytes_used_disk"] = numBytesUsedDisk + // total disk bytes by the latest root index, exclusive of older snapshots + m["num_bytes_used_disk_by_root"] = numBytesOnDiskByRoot + // num_bytes_used_disk_by_root_reclaimable is an approximation about the + // reclaimable disk space in an index. (eg: from a full compaction) + m["num_bytes_used_disk_by_root_reclaimable"] = uint64(float64(numBytesOnDiskByRoot) * + indexSnapshot.reClaimableDocsRatio()) + m["num_files_on_disk"] = numFilesOnDisk + m["num_root_memorysegments"] = m["TotMemorySegmentsAtRoot"] + m["num_root_filesegments"] = m["TotFileSegmentsAtRoot"] + m["num_persister_nap_pause_completed"] = m["TotPersisterNapPauseCompleted"] + m["num_persister_nap_merger_break"] = m["TotPersisterMergerNapBreak"] + m["total_compaction_written_bytes"] = m["TotFileMergeWrittenBytes"] + + // the bool stat `index_bgthreads_active` indicates whether the background routines + // (which are responsible for the index to attain a steady state) are still + // doing some work. + if rootEpoch, ok := m["CurRootEpoch"].(uint64); ok { + if lastMergedEpoch, ok := m["LastMergedEpoch"].(uint64); ok { + if lastPersistedEpoch, ok := m["LastPersistedEpoch"].(uint64); ok { + m["index_bgthreads_active"] = !(lastMergedEpoch == rootEpoch && lastPersistedEpoch == rootEpoch) + } + } + } + + // calculate the aggregate of all the segment's field stats + aggFieldStats := newFieldStats() + for _, segmentSnapshot := range indexSnapshot.Segments() { + if segmentSnapshot.stats != nil { + aggFieldStats.Aggregate(segmentSnapshot.stats) + } + } + + aggFieldStatsMap := aggFieldStats.Fetch() + for statName, stats := range aggFieldStatsMap { + for fieldName, val := range stats { + m["field:"+fieldName+":"+statName] = val + } + } + return m +} + +func (s *Scorch) Analyze(d index.Document) { + analyze(d, s.setSpatialAnalyzerPlugin) +} + +type customAnalyzerPluginInitFunc func(field index.Field) + +func (s *Scorch) setSpatialAnalyzerPlugin(f index.Field) { + if s.segPlugin != nil { + // check whether the current field is a custom tokenizable + // spatial field then set the spatial analyser plugin for + // overriding the tokenisation during the analysis stage. + if sf, ok := f.(index.TokenizableSpatialField); ok { + sf.SetSpatialAnalyzerPlugin(s.spatialPlugin) + } + } +} + +func analyze(d index.Document, fn customAnalyzerPluginInitFunc) { + d.VisitFields(func(field index.Field) { + if field.Options().IsIndexed() { + if fn != nil { + fn(field) + } + + field.Analyze() + + if d.HasComposite() && field.Name() != "_id" { + // see if any of the composite fields need this + d.VisitComposite(func(cf index.CompositeField) { + cf.Compose(field.Name(), field.AnalyzedLength(), field.AnalyzedTokenFrequencies()) + }) + // Since the encoded geoShape is only necessary within the doc values + // of the geoShapeField, it has been removed from the field's term dictionary. + // However, '_all' field uses its term dictionary as its docValues, so it + // becomes necessary to add the geoShape into the '_all' field's term dictionary + if f, ok := field.(index.GeoShapeField); ok { + d.VisitComposite(func(cf index.CompositeField) { + geoshape := f.EncodedShape() + cf.Compose(field.Name(), 1, index.TokenFrequencies{ + string(geoshape): &index.TokenFreq{ + Term: geoshape, + Locations: []*index.TokenLocation{ + { + Start: 0, + End: len(geoshape), + Position: 1, + }, + }, + }, + }) + }) + } + } + } + }) +} + +func (s *Scorch) AddEligibleForRemoval(epoch uint64) { + s.rootLock.Lock() + if s.root == nil || s.root.epoch != epoch { + s.eligibleForRemoval = append(s.eligibleForRemoval, epoch) + } + s.rootLock.Unlock() +} + +func (s *Scorch) MemoryUsed() (memUsed uint64) { + indexSnapshot := s.currentSnapshot() + if indexSnapshot == nil { + return + } + + defer func() { + _ = indexSnapshot.Close() + }() + + // Account for current root snapshot overhead + memUsed += uint64(indexSnapshot.Size()) + + // Account for snapshot that the persister may be working on + persistEpoch := atomic.LoadUint64(&s.iStats.persistEpoch) + persistSnapshotSize := atomic.LoadUint64(&s.iStats.persistSnapshotSize) + if persistEpoch != 0 && indexSnapshot.epoch > persistEpoch { + // the snapshot that the persister is working on isn't the same as + // the current snapshot + memUsed += persistSnapshotSize + } + + // Account for snapshot that the merger may be working on + mergeEpoch := atomic.LoadUint64(&s.iStats.mergeEpoch) + mergeSnapshotSize := atomic.LoadUint64(&s.iStats.mergeSnapshotSize) + if mergeEpoch != 0 && indexSnapshot.epoch > mergeEpoch { + // the snapshot that the merger is working on isn't the same as + // the current snapshot + memUsed += mergeSnapshotSize + } + + memUsed += (atomic.LoadUint64(&s.iStats.newSegBufBytesAdded) - + atomic.LoadUint64(&s.iStats.newSegBufBytesRemoved)) + + memUsed += (atomic.LoadUint64(&s.iStats.analysisBytesAdded) - + atomic.LoadUint64(&s.iStats.analysisBytesRemoved)) + + return memUsed +} + +func (s *Scorch) markIneligibleForRemoval(filename string) { + s.rootLock.Lock() + s.ineligibleForRemoval[filename] = true + s.rootLock.Unlock() +} + +func (s *Scorch) unmarkIneligibleForRemoval(filename string) { + s.rootLock.Lock() + delete(s.ineligibleForRemoval, filename) + s.rootLock.Unlock() +} + +func init() { + err := registry.RegisterIndexType(Name, NewScorch) + if err != nil { + panic(err) + } +} + +func parseToTimeDuration(i interface{}) (time.Duration, error) { + switch v := i.(type) { + case string: + return time.ParseDuration(v) + + default: + return 0, fmt.Errorf("expects a duration string") + } +} + +func parseToInteger(i interface{}) (int, error) { + switch v := i.(type) { + case float64: + return int(v), nil + case int: + return v, nil + + default: + return 0, fmt.Errorf("expects int or float64 value") + } +} + +// Holds Zap's field level stats at a segment level +type fieldStats struct { + // StatName -> FieldName -> value + statMap map[string]map[string]uint64 +} + +// Add the data into the map after checking if the statname is valid +func (fs *fieldStats) Store(statName, fieldName string, value uint64) { + if _, exists := fs.statMap[statName]; !exists { + fs.statMap[statName] = make(map[string]uint64) + } + fs.statMap[statName][fieldName] = value +} + +// Combine the given stats map with the existing map +func (fs *fieldStats) Aggregate(stats segment.FieldStats) { + statMap := stats.Fetch() + if statMap == nil { + return + } + for statName, statMap := range statMap { + if _, exists := fs.statMap[statName]; !exists { + fs.statMap[statName] = make(map[string]uint64) + } + for fieldName, val := range statMap { + if _, exists := fs.statMap[statName][fieldName]; !exists { + fs.statMap[statName][fieldName] = 0 + } + fs.statMap[statName][fieldName] += val + } + } +} + +// Returns the stats map +func (fs *fieldStats) Fetch() map[string]map[string]uint64 { + if fs == nil { + return nil + } + + return fs.statMap +} + +// Initializes an empty stats map +func newFieldStats() *fieldStats { + rv := &fieldStats{ + statMap: map[string]map[string]uint64{}, + } + return rv +} + +// CopyReader returns a low-level accessor for index data, ensuring persisted segments +// remain on disk for backup, preventing race conditions with the persister/merger cleanup. +// Close the reader after backup to allow segment removal by the persister/merger. +func (s *Scorch) CopyReader() index.CopyReader { + s.rootLock.Lock() + rv := s.root + if rv != nil { + rv.AddRef() + var fileName string + // schedule a backup for all the segments from the root. Note that the + // both the unpersisted and persisted segments are scheduled for backup. + // because during the backup, the unpersisted segments may get persisted and + // hence we need to protect both the unpersisted and persisted segments from removal + // by the cleanup routine during the online backup + for _, seg := range rv.segment { + if perSeg, ok := seg.segment.(segment.PersistedSegment); ok { + // segment is persisted + fileName = filepath.Base(perSeg.Path()) + } else { + // segment is not persisted + // the name of the segment file that is generated if the + // the segment is persisted in the future. + fileName = zapFileName(seg.id) + } + rv.parent.copyScheduled[fileName]++ + } + } + s.rootLock.Unlock() + return rv +} + +// external API to fire a scorch event (EventKindIndexStart) externally from bleve +func (s *Scorch) FireIndexEvent() { + s.fireEvent(EventKindIndexStart, 0) +} diff --git a/index/scorch/scorch_test.go b/index/scorch/scorch_test.go new file mode 100644 index 0000000..0c2ffbe --- /dev/null +++ b/index/scorch/scorch_test.go @@ -0,0 +1,2812 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "context" + "encoding/json" + "fmt" + "log" + "math/rand" + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" + "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + regexpTokenizer "github.com/blevesearch/bleve/v2/analysis/tokenizer/regexp" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/scorch/mergeplan" + "github.com/blevesearch/bleve/v2/mapping" + index "github.com/blevesearch/bleve_index_api" +) + +func init() { + // override for tests + DefaultPersisterNapTimeMSec = 1 +} + +func InitTest(cfg map[string]interface{}) error { + return os.RemoveAll(cfg["path"].(string)) +} + +func DestroyTest(cfg map[string]interface{}) error { + return os.RemoveAll(cfg["path"].(string)) +} + +func CreateConfig(name string) map[string]interface{} { + // TODO: Use t.Name() when Go 1.7 support terminates. + rv := make(map[string]interface{}) + rv["path"] = os.TempDir() + "/bleve-scorch-test-" + name + return rv +} + +var testAnalyzer = &analysis.DefaultAnalyzer{ + Tokenizer: regexpTokenizer.NewRegexpTokenizer(regexp.MustCompile(`\w+`)), +} + +func TestIndexOpenReopen(t *testing.T) { + cfg := CreateConfig("TestIndexOpenReopen") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // insert a doc + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now close it + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + idx, err = NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + // check the doc count again after reopening it + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now close it + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexOpenReopenWithInsert(t *testing.T) { + cfg := CreateConfig("TestIndexOpenReopen") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // insert a doc + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now close it + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + // try to open the index and insert data + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + // insert a doc + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + // check the doc count again after reopening it + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now close it + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsert(t *testing.T) { + cfg := CreateConfig("TestIndexInsert") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsertThenDelete(t *testing.T) { + cfg := CreateConfig("TestIndexInsertThenDelete") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc2 := document.NewDocument("2") + doc2.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc2) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + iid, err := reader.InternalID("1") + if err != nil || iid == nil { + t.Errorf("unexpected on doc id 1") + } + iid, err = reader.InternalID("2") + if err != nil || iid == nil { + t.Errorf("unexpected on doc id 2") + } + iid, err = reader.InternalID("3") + if err != nil || iid != nil { + t.Errorf("unexpected on doc id 3") + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + err = idx.Delete("1") + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + expectedCount-- + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + storedDoc, err := reader.Document("1") + if err != nil { + t.Error(err) + } + if storedDoc != nil { + t.Errorf("expected nil for deleted stored doc #1, got %v", storedDoc) + } + storedDoc, err = reader.Document("2") + if err != nil { + t.Error(err) + } + if storedDoc == nil { + t.Errorf("expected stored doc for #2, got nil") + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now close it + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + idx, err = NewScorch(Name, cfg, analysisQueue) // reopen + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error reopening index: %v", err) + } + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + storedDoc, err = reader.Document("1") + if err != nil { + t.Error(err) + } + if storedDoc != nil { + t.Errorf("expected nil for deleted stored doc #1, got %v", storedDoc) + } + storedDoc, err = reader.Document("2") + if err != nil { + t.Error(err) + } + if storedDoc == nil { + t.Errorf("expected stored doc for #2, got nil") + } + iid, err = reader.InternalID("1") + if err != nil || iid != nil { + t.Errorf("unexpected on doc id 1") + } + iid, err = reader.InternalID("2") + if err != nil || iid == nil { + t.Errorf("unexpected on doc id 2, should exist") + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + err = idx.Delete("2") + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + expectedCount-- + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + storedDoc, err = reader.Document("1") + if err != nil { + t.Error(err) + } + if storedDoc != nil { + t.Errorf("expected nil for deleted stored doc #1, got %v", storedDoc) + } + storedDoc, err = reader.Document("2") + if err != nil { + t.Error(err) + } + if storedDoc != nil { + t.Errorf("expected nil for deleted stored doc #2, got nil") + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsertThenUpdate(t *testing.T) { + cfg := CreateConfig("TestIndexInsertThenUpdate") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + + var expectedCount uint64 + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + // this update should overwrite one term, and introduce one new one + doc = document.NewDocument("1") + doc.AddField(document.NewTextFieldWithAnalyzer("name", []uint64{}, []byte("test fail"), testAnalyzer)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + + // now do another update that should remove one of the terms + doc = document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("fail"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsertMultiple(t *testing.T) { + cfg := CreateConfig("TestIndexInsertMultiple") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("3") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsertWithStore(t *testing.T) { + cfg := CreateConfig("TestIndexInsertWithStore") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + storedDocInt, err := indexReader.Document("1") + if err != nil { + t.Error(err) + } + + storedDoc := storedDocInt.(*document.Document) + + if len(storedDoc.Fields) != 1 { + t.Errorf("expected 1 stored field, got %d", len(storedDoc.Fields)) + } + for _, field := range storedDoc.Fields { + if field.Name() == "name" { + textField, ok := field.(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "test" { + t.Errorf("expected field content 'test', got '%s'", string(textField.Value())) + } + } else if field.Name() == "_id" { + t.Errorf("not expecting _id field") + } + } +} + +func TestIndexInternalCRUD(t *testing.T) { + cfg := CreateConfig("TestIndexInternalCRUD") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + + if len(indexReader.(*IndexSnapshot).segment) != 0 { + t.Errorf("expected 0 segments") + } + + // get something that doesn't exist yet + val, err := indexReader.GetInternal([]byte("key")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got %s", val) + } + + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + + // set + err = idx.SetInternal([]byte("key"), []byte("abc")) + if err != nil { + t.Error(err) + } + + indexReader2, err := idx.Reader() + if err != nil { + t.Error(err) + } + + if len(indexReader2.(*IndexSnapshot).segment) != 0 { + t.Errorf("expected 0 segments") + } + + // get + val, err = indexReader2.GetInternal([]byte("key")) + if err != nil { + t.Error(err) + } + if string(val) != "abc" { + t.Errorf("expected %s, got '%s'", "abc", val) + } + + err = indexReader2.Close() + if err != nil { + t.Fatal(err) + } + + // delete + err = idx.DeleteInternal([]byte("key")) + if err != nil { + t.Error(err) + } + + indexReader3, err := idx.Reader() + if err != nil { + t.Error(err) + } + + if len(indexReader3.(*IndexSnapshot).segment) != 0 { + t.Errorf("expected 0 segments") + } + + // get again + val, err = indexReader3.GetInternal([]byte("key")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got %s", val) + } + + err = indexReader3.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexBatch(t *testing.T) { + cfg := CreateConfig("TestIndexBatch") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + + // first create 2 docs the old fashioned way + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + // now create a batch which does 3 things + // insert new doc + // update existing doc + // delete existing doc + // net document count change 0 + + batch := index.NewBatch() + doc = document.NewDocument("3") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test3"))) + batch.Update(doc) + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2updated"))) + batch.Update(doc) + batch.Delete("1") + + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + numSegments := len(indexReader.(*IndexSnapshot).segment) + if numSegments <= 0 { + t.Errorf("expected some segments, got: %d", numSegments) + } + + docCount, err := indexReader.DocCount() + if err != nil { + t.Fatal(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + + docIDReader, err := indexReader.DocIDReaderAll() + if err != nil { + t.Error(err) + } + var docIds []index.IndexInternalID + docID, err := docIDReader.Next() + for docID != nil && err == nil { + docIds = append(docIds, docID) + docID, err = docIDReader.Next() + } + if err != nil { + t.Error(err) + } + externalDocIds := map[string]struct{}{} + // convert back to external doc ids + for _, id := range docIds { + externalID, err := indexReader.ExternalID(id) + if err != nil { + t.Fatal(err) + } + externalDocIds[externalID] = struct{}{} + } + expectedDocIds := map[string]struct{}{ + "2": {}, + "3": {}, + } + if !reflect.DeepEqual(externalDocIds, expectedDocIds) { + t.Errorf("expected ids: %v, got ids: %v", expectedDocIds, externalDocIds) + } +} + +func TestIndexBatchWithCallbacks(t *testing.T) { + cfg := CreateConfig("TestIndexBatchWithCallbacks") + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + cerr := idx.Close() + if cerr != nil { + t.Fatal(cerr) + } + }() + + // Check that callback function works + var wg sync.WaitGroup + wg.Add(1) + + batch := index.NewBatch() + doc := document.NewDocument("3") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test3"))) + batch.Update(doc) + batch.SetPersistedCallback(func(e error) { + wg.Done() + }) + + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + + wg.Wait() + // test has no assertion but will timeout if callback doesn't fire +} + +func TestIndexInsertUpdateDeleteWithMultipleTypesStored(t *testing.T) { + cfg := CreateConfig("TestIndexInsertUpdateDeleteWithMultipleTypesStored") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 35.99, index.IndexField|index.StoreField)) + df, err := document.NewDateTimeFieldWithIndexingOptions("unixEpoch", []uint64{}, time.Unix(0, 0), time.RFC3339, index.IndexField|index.StoreField) + if err != nil { + t.Error(err) + } + doc.AddField(df) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + + storedDocInt, err := indexReader.Document("1") + if err != nil { + t.Error(err) + } + + storedDoc := storedDocInt.(*document.Document) + + err = indexReader.Close() + if err != nil { + t.Error(err) + } + + if len(storedDoc.Fields) != 3 { + t.Errorf("expected 3 stored field, got %d", len(storedDoc.Fields)) + } + for _, field := range storedDoc.Fields { + if field.Name() == "name" { + textField, ok := field.(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "test" { + t.Errorf("expected field content 'test', got '%s'", string(textField.Value())) + } + } else if field.Name() == "age" { + numField, ok := field.(*document.NumericField) + if !ok { + t.Errorf("expected numeric field") + } + numFieldNumer, err := numField.Number() + if err != nil { + t.Error(err) + } else { + if numFieldNumer != 35.99 { + t.Errorf("expected numeric value 35.99, got %f", numFieldNumer) + } + } + } else if field.Name() == "unixEpoch" { + dateField, ok := field.(*document.DateTimeField) + if !ok { + t.Errorf("expected date field") + } + dateFieldDate, _, err := dateField.DateTime() + if err != nil { + t.Error(err) + } else { + if dateFieldDate != time.Unix(0, 0).UTC() { + t.Errorf("expected date value unix epoch, got %v", dateFieldDate) + } + } + } else if field.Name() == "_id" { + t.Errorf("not expecting _id field") + } + } + + // now update the document, but omit one of the fields + doc = document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("testup"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 36.99, index.IndexField|index.StoreField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader2, err := idx.Reader() + if err != nil { + t.Error(err) + } + + // expected doc count shouldn't have changed + docCount, err = indexReader2.DocCount() + if err != nil { + t.Fatal(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + + // should only get 2 fields back now though + storedDocInt, err = indexReader2.Document("1") + if err != nil { + t.Error(err) + } + + storedDoc = storedDocInt.(*document.Document) + + err = indexReader2.Close() + if err != nil { + t.Error(err) + } + + if len(storedDoc.Fields) != 2 { + t.Errorf("expected 2 stored field, got %d", len(storedDoc.Fields)) + } + + for _, field := range storedDoc.Fields { + if field.Name() == "name" { + textField, ok := field.(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "testup" { + t.Errorf("expected field content 'testup', got '%s'", string(textField.Value())) + } + } else if field.Name() == "age" { + numField, ok := field.(*document.NumericField) + if !ok { + t.Errorf("expected numeric field") + } + numFieldNumer, err := numField.Number() + if err != nil { + t.Error(err) + } else { + if numFieldNumer != 36.99 { + t.Errorf("expected numeric value 36.99, got %f", numFieldNumer) + } + } + } else if field.Name() == "_id" { + t.Errorf("not expecting _id field") + } + } + + // now delete the document + err = idx.Delete("1") + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + expectedCount-- + + // expected doc count shouldn't have changed + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsertFields(t *testing.T) { + cfg := CreateConfig("TestIndexInsertFields") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 35.99, index.IndexField|index.StoreField)) + dateField, err := document.NewDateTimeFieldWithIndexingOptions("unixEpoch", []uint64{}, time.Unix(0, 0), time.RFC3339, index.IndexField|index.StoreField) + if err != nil { + t.Error(err) + } + doc.AddField(dateField) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + fields, err := indexReader.Fields() + if err != nil { + t.Error(err) + } else { + fieldsMap := map[string]struct{}{} + for _, field := range fields { + fieldsMap[field] = struct{}{} + } + expectedFieldsMap := map[string]struct{}{ + "_id": {}, + "name": {}, + "age": {}, + "unixEpoch": {}, + } + if !reflect.DeepEqual(fieldsMap, expectedFieldsMap) { + t.Errorf("expected fields: %v, got %v", expectedFieldsMap, fieldsMap) + } + } +} + +func TestIndexUpdateComposites(t *testing.T) { + cfg := CreateConfig("TestIndexUpdateComposites") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField)) + doc.AddField(document.NewCompositeFieldWithIndexingOptions("_all", true, nil, nil, index.IndexField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + // now lets update it + doc = document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("testupdated"), index.IndexField|index.StoreField)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("misterupdated"), index.IndexField|index.StoreField)) + doc.AddField(document.NewCompositeFieldWithIndexingOptions("_all", true, nil, nil, index.IndexField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + // make sure new values are in index + storedDocInt, err := indexReader.Document("1") + if err != nil { + t.Error(err) + } + storedDoc := storedDocInt.(*document.Document) + if len(storedDoc.Fields) != 2 { + t.Errorf("expected 2 stored field, got %d", len(storedDoc.Fields)) + } + for _, field := range storedDoc.Fields { + if field.Name() == "name" { + textField, ok := field.(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "testupdated" { + t.Errorf("expected field content 'test', got '%s'", string(textField.Value())) + } + } else if field.Name() == "_id" { + t.Errorf("not expecting _id field") + } + } +} + +func TestIndexTermReaderCompositeFields(t *testing.T) { + cfg := CreateConfig("TestIndexTermReaderCompositeFields") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc.AddField(document.NewCompositeFieldWithIndexingOptions("_all", true, nil, nil, index.IndexField|index.IncludeTermVectors)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + termFieldReader, err := indexReader.TermFieldReader(context.TODO(), []byte("mister"), "_all", true, true, true) + if err != nil { + t.Error(err) + } + + tfd, err := termFieldReader.Next(nil) + for tfd != nil && err == nil { + externalID, err := indexReader.ExternalID(tfd.ID) + if err != nil { + t.Fatal(err) + } + + if externalID != "1" { + t.Errorf("expected to find document id 1") + } + + tfd, err = termFieldReader.Next(nil) + if err != nil { + t.Error(err) + } + } + if err != nil { + t.Error(err) + } +} + +func TestIndexDocValueReader(t *testing.T) { + cfg := CreateConfig("TestIndexDocumentVisitFieldTerms") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + actualFieldTerms := make(fieldTerms) + + internalID, err := indexReader.InternalID("1") + if err != nil { + t.Fatal(err) + } + + dvr, err := indexReader.DocValueReader([]string{"name", "title"}) + if err != nil { + t.Error(err) + } + + err = dvr.VisitDocValues(internalID, func(field string, term []byte) { + actualFieldTerms[field] = append(actualFieldTerms[field], string(term)) + }) + if err != nil { + t.Error(err) + } + expectedFieldTerms := fieldTerms{ + "name": []string{"test"}, + "title": []string{"mister"}, + } + if !reflect.DeepEqual(actualFieldTerms, expectedFieldTerms) { + t.Errorf("expected field terms: %#v, got: %#v", expectedFieldTerms, actualFieldTerms) + } +} + +func TestDocValueReaderConcurrent(t *testing.T) { + cfg := CreateConfig("TestFieldTermsConcurrent") + + // setting path to empty string disables persistence/merging + // which ensures we have in-memory segments + // which is important for this test, to trigger the right code + // path, where fields exist, but have NOT been uninverted by + // the Segment impl (in memory segments are still SegmentBase) + cfg["path"] = "" + + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Fatal(err) + } + }() + + mp := mapping.NewIndexMapping() + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + cerr := idx.Close() + if cerr != nil { + t.Fatal(cerr) + } + }() + + // create a single bath (leading to 1 in-memory segment) + // have one field named "name" and 100 others named f0-f99 + batch := index.NewBatch() + for i := 0; i < 1000; i++ { + data := map[string]string{ + "name": fmt.Sprintf("doc-%d", i), + } + for j := 0; j < 100; j++ { + data[fmt.Sprintf("f%d", j)] = fmt.Sprintf("v%d", i) + } + doc := document.NewDocument(fmt.Sprintf("%d", i)) + err = mp.MapDocument(doc, data) + if err != nil { + t.Errorf("error mapping doc: %v", err) + } + batch.Update(doc) + } + + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + // now have 10 goroutines try to visit field values for doc 1 + // in a random field + var wg sync.WaitGroup + for j := 0; j < 10; j++ { + wg.Add(1) + go func() { + r, err := idx.Reader() + if err != nil { + t.Errorf("error getting reader: %v", err) + wg.Done() + return + } + docNumber, err := r.InternalID("1") + if err != nil { + t.Errorf("error getting internal ID: %v", err) + wg.Done() + return + } + dvr, err := r.DocValueReader([]string{fmt.Sprintf("f%d", rand.Intn(100))}) + if err != nil { + t.Errorf("error getting doc value reader: %v", err) + wg.Done() + return + } + err = dvr.VisitDocValues(docNumber, func(field string, term []byte) {}) + if err != nil { + t.Errorf("error visiting doc values: %v", err) + wg.Done() + return + } + wg.Done() + }() + } + + wg.Wait() +} + +func TestConcurrentUpdate(t *testing.T) { + cfg := CreateConfig("TestConcurrentUpdate") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // do some concurrent updates + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(i int) { + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions(strconv.Itoa(i), []uint64{}, []byte(strconv.Itoa(i)), index.StoreField)) + err := idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + wg.Done() + }(i) + } + wg.Wait() + + // now load the name field and see what we get + r, err := idx.Reader() + if err != nil { + log.Fatal(err) + } + defer func() { + err := r.Close() + if err != nil { + t.Fatal(err) + } + }() + + docInt, err := r.Document("1") + if err != nil { + log.Fatal(err) + } + + doc := docInt.(*document.Document) + + if len(doc.Fields) > 2 { + t.Errorf("expected no more than 2 fields, found %d", len(doc.Fields)) + } +} + +func TestLargeField(t *testing.T) { + cfg := CreateConfig("TestLargeField") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var largeFieldValue []byte + for len(largeFieldValue) < 4096 { + largeFieldValue = append(largeFieldValue, bleveWikiArticle1K...) + } + + d := document.NewDocument("large") + f := document.NewTextFieldWithIndexingOptions("desc", nil, largeFieldValue, index.IndexField|index.StoreField) + d.AddField(f) + + err = idx.Update(d) + if err != nil { + t.Fatal(err) + } +} + +var bleveWikiArticle1K = []byte(`Boiling liquid expanding vapor explosion +From Wikipedia, the free encyclopedia +See also: Boiler explosion and Steam explosion + +Flames subsequent to a flammable liquid BLEVE from a tanker. BLEVEs do not necessarily involve fire. + +This article's tone or style may not reflect the encyclopedic tone used on Wikipedia. See Wikipedia's guide to writing better articles for suggestions. (July 2013) +A boiling liquid expanding vapor explosion (BLEVE, /ˈblɛviː/ blev-ee) is an explosion caused by the rupture of a vessel containing a pressurized liquid above its boiling point.[1] +Contents [hide] +1 Mechanism +1.1 Water example +1.2 BLEVEs without chemical reactions +2 Fires +3 Incidents +4 Safety measures +5 See also +6 References +7 External links +Mechanism[edit] + +This section needs additional citations for verification. Please help improve this article by adding citations to reliable sources. Unsourced material may be challenged and removed. (July 2013) +There are three characteristics of liquids which are relevant to the discussion of a BLEVE:`) + +func TestIndexDocValueReaderWithMultipleDocs(t *testing.T) { + cfg := CreateConfig("TestIndexDocumentVisitFieldTermsWithMultipleDocs") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + + actualFieldTerms := make(fieldTerms) + docNumber, err := indexReader.InternalID("1") + if err != nil { + t.Fatal(err) + } + + dvr, err := indexReader.DocValueReader([]string{"name", "title"}) + if err != nil { + t.Fatal(err) + } + + err = dvr.VisitDocValues(docNumber, func(field string, term []byte) { + actualFieldTerms[field] = append(actualFieldTerms[field], string(term)) + }) + if err != nil { + t.Error(err) + } + expectedFieldTerms := fieldTerms{ + "name": []string{"test"}, + "title": []string{"mister"}, + } + if !reflect.DeepEqual(actualFieldTerms, expectedFieldTerms) { + t.Errorf("expected field terms: %#v, got: %#v", expectedFieldTerms, actualFieldTerms) + } + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + + doc2 := document.NewDocument("2") + doc2.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test2"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc2.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister2"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + err = idx.Update(doc2) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + indexReader, err = idx.Reader() + if err != nil { + t.Error(err) + } + + actualFieldTerms = make(fieldTerms) + docNumber, err = indexReader.InternalID("2") + if err != nil { + t.Fatal(err) + } + + dvr, err = indexReader.DocValueReader([]string{"name", "title"}) + if err != nil { + t.Fatal(err) + } + + err = dvr.VisitDocValues(docNumber, func(field string, term []byte) { + actualFieldTerms[field] = append(actualFieldTerms[field], string(term)) + }) + if err != nil { + t.Error(err) + } + expectedFieldTerms = fieldTerms{ + "name": []string{"test2"}, + "title": []string{"mister2"}, + } + if !reflect.DeepEqual(actualFieldTerms, expectedFieldTerms) { + t.Errorf("expected field terms: %#v, got: %#v", expectedFieldTerms, actualFieldTerms) + } + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + + doc3 := document.NewDocument("3") + doc3.AddField(document.NewTextFieldWithIndexingOptions("name3", []uint64{}, []byte("test3"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc3.AddField(document.NewTextFieldWithIndexingOptions("title3", []uint64{}, []byte("mister3"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + err = idx.Update(doc3) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + indexReader, err = idx.Reader() + if err != nil { + t.Error(err) + } + + actualFieldTerms = make(fieldTerms) + docNumber, err = indexReader.InternalID("3") + if err != nil { + t.Fatal(err) + } + + dvr, err = indexReader.DocValueReader([]string{"name3", "title3"}) + if err != nil { + t.Fatal(err) + } + + err = dvr.VisitDocValues(docNumber, func(field string, term []byte) { + actualFieldTerms[field] = append(actualFieldTerms[field], string(term)) + }) + if err != nil { + t.Error(err) + } + expectedFieldTerms = fieldTerms{ + "name3": []string{"test3"}, + "title3": []string{"mister3"}, + } + if !reflect.DeepEqual(actualFieldTerms, expectedFieldTerms) { + t.Errorf("expected field terms: %#v, got: %#v", expectedFieldTerms, actualFieldTerms) + } + + actualFieldTerms = make(fieldTerms) + docNumber, err = indexReader.InternalID("1") + if err != nil { + t.Fatal(err) + } + + dvr, err = indexReader.DocValueReader([]string{"name", "title"}) + if err != nil { + t.Fatal(err) + } + + err = dvr.VisitDocValues(docNumber, func(field string, term []byte) { + actualFieldTerms[field] = append(actualFieldTerms[field], string(term)) + }) + if err != nil { + t.Error(err) + } + expectedFieldTerms = fieldTerms{ + "name": []string{"test"}, + "title": []string{"mister"}, + } + if !reflect.DeepEqual(actualFieldTerms, expectedFieldTerms) { + t.Errorf("expected field terms: %#v, got: %#v", expectedFieldTerms, actualFieldTerms) + } + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexDocValueReaderWithMultipleFieldOptions(t *testing.T) { + cfg := CreateConfig("TestIndexDocumentVisitFieldTermsWithMultipleFieldOptions") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // mix of field options, this exercises the run time/ on the fly un inverting of + // doc values for custom options enabled field like designation, dept. + options := index.IndexField | index.StoreField | index.IncludeTermVectors + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) // default doc value persisted + doc.AddField(document.NewTextField("title", []uint64{}, []byte("mister"))) // default doc value persisted + doc.AddField(document.NewTextFieldWithIndexingOptions("designation", []uint64{}, []byte("engineer"), options)) + doc.AddField(document.NewTextFieldWithIndexingOptions("dept", []uint64{}, []byte("bleve"), options)) + + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + + actualFieldTerms := make(fieldTerms) + docNumber, err := indexReader.InternalID("1") + if err != nil { + t.Fatal(err) + } + + dvr, err := indexReader.DocValueReader([]string{"name", "designation", "dept"}) + if err != nil { + t.Fatal(err) + } + + err = dvr.VisitDocValues(docNumber, func(field string, term []byte) { + actualFieldTerms[field] = append(actualFieldTerms[field], string(term)) + }) + if err != nil { + t.Error(err) + } + expectedFieldTerms := fieldTerms{ + "name": []string{"test"}, + "designation": []string{"engineer"}, + "dept": []string{"bleve"}, + } + if !reflect.DeepEqual(actualFieldTerms, expectedFieldTerms) { + t.Errorf("expected field terms: %#v, got: %#v", expectedFieldTerms, actualFieldTerms) + } + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestAllFieldWithDifferentTermVectorsEnabled(t *testing.T) { + // Based on https://github.com/blevesearch/bleve/issues/895 from xeizmendi + cfg := CreateConfig("TestAllFieldWithDifferentTermVectorsEnabled") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + testConfig := cfg + mp := mapping.NewIndexMapping() + + keywordMapping := mapping.NewTextFieldMapping() + keywordMapping.Analyzer = keyword.Name + keywordMapping.IncludeTermVectors = false + keywordMapping.IncludeInAll = true + + textMapping := mapping.NewTextFieldMapping() + textMapping.Analyzer = standard.Name + textMapping.IncludeTermVectors = true + textMapping.IncludeInAll = true + + docMapping := mapping.NewDocumentStaticMapping() + docMapping.AddFieldMappingsAt("keyword", keywordMapping) + docMapping.AddFieldMappingsAt("text", textMapping) + + mp.DefaultMapping = docMapping + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch("storeName", testConfig, analysisQueue) + if err != nil { + log.Fatalln(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + data := map[string]string{ + "keyword": "something", + "text": "A sentence that includes something within.", + } + + doc := document.NewDocument("1") + err = mp.MapDocument(doc, data) + if err != nil { + t.Errorf("error mapping doc: %v", err) + } + + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } +} + +func TestForceVersion(t *testing.T) { + cfg := map[string]interface{}{} + cfg["forceSegmentType"] = "zap" + cfg["forceSegmentVersion"] = 11 + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatalf("error opening a supported vesion: %v", err) + } + s := idx.(*Scorch) + if s.segPlugin.Version() != 11 { + t.Fatalf("wrong segment wrapper version loaded, expected %d got %d", 11, s.segPlugin.Version()) + } + cfg["forceSegmentVersion"] = 12 + idx, err = NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatalf("error opening a supported vesion: %v", err) + } + s = idx.(*Scorch) + if s.segPlugin.Version() != 12 { + t.Fatalf("wrong segment wrapper version loaded, expected %d got %d", 12, s.segPlugin.Version()) + } + cfg["forceSegmentVersion"] = 10 + _, err = NewScorch(Name, cfg, analysisQueue) + if err == nil { + t.Fatalf("expected an error opening an unsupported vesion, got nil") + } +} + +func TestIndexForceMerge(t *testing.T) { + cfg := CreateConfig("TestIndexForceMerge") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + tmp := struct { + MaxSegmentsPerTier int `json:"maxSegmentsPerTier"` + SegmentsPerMergeTask int `json:"segmentsPerMergeTask"` + FloorSegmentSize int64 `json:"floorSegmentSize"` + }{ + int(1), + int(1), + int64(2), + } + cfg["scorchMergePlanOptions"] = &tmp + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + + var expectedCount uint64 + batch := index.NewBatch() + for i := 0; i < 10; i++ { + doc := document.NewDocument(fmt.Sprintf("doc1-%d", i)) + doc.AddField(document.NewTextField("name", []uint64{}, []byte(fmt.Sprintf("text1-%d", i)))) + batch.Update(doc) + doc = document.NewDocument(fmt.Sprintf("doc2-%d", i)) + doc.AddField(document.NewTextField("name", []uint64{}, []byte(fmt.Sprintf("text2-%d", i)))) + batch.Update(doc) + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + batch.Reset() + expectedCount += 2 + } + + // verify doc count + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + + docCount, err := indexReader.DocCount() + if err != nil { + t.Fatal(err) + } + + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + + var si *Scorch + var ok bool + if si, ok = idx.(*Scorch); !ok { + t.Errorf("expects a scorch index") + } + + nfs := atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot) + if nfs != 10 { + t.Errorf("expected 10 root file segments, got: %d", nfs) + } + + ctx := context.Background() + for atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot) != 1 { + err := si.ForceMerge(ctx, &mergeplan.MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 10000, + SegmentsPerMergeTask: 10, + FloorSegmentSize: 10000, + }) + if err != nil { + t.Errorf("ForceMerge failed, err: %v", err) + } + } + + // verify the final root segment count + if atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot) != 1 { + t.Errorf("expected a single root file segments, got: %d", + atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot)) + } + + // verify with an invalid merge plan + err = si.ForceMerge(ctx, &mergeplan.MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 1 << 33, + SegmentsPerMergeTask: 10, + FloorSegmentSize: 10000, + }) + if err != mergeplan.ErrMaxSegmentSizeTooLarge { + t.Errorf("ForceMerge expected to fail with ErrMaxSegmentSizeTooLarge") + } + + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestCancelIndexForceMerge(t *testing.T) { + cfg := CreateConfig("TestCancelIndexForceMerge") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + tmp := struct { + MaxSegmentsPerTier int `json:"maxSegmentsPerTier"` + SegmentsPerMergeTask int `json:"segmentsPerMergeTask"` + FloorSegmentSize int64 `json:"floorSegmentSize"` + }{ + int(1), + int(1), + int64(2), + } + cfg["scorchMergePlanOptions"] = &tmp + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + + var expectedCount uint64 + batch := index.NewBatch() + for i := 0; i < 20; i++ { + doc := document.NewDocument(fmt.Sprintf("doc1-%d", i)) + doc.AddField(document.NewTextField("name", []uint64{}, []byte(fmt.Sprintf("text1-%d", i)))) + batch.Update(doc) + doc = document.NewDocument(fmt.Sprintf("doc2-%d", i)) + doc.AddField(document.NewTextField("name", []uint64{}, []byte(fmt.Sprintf("text2-%d", i)))) + batch.Update(doc) + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + batch.Reset() + expectedCount += 2 + } + + // verify doc count + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + docCount, err := indexReader.DocCount() + if err != nil { + t.Fatal(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + + var si *Scorch + var ok bool + if si, ok = idx.(*Scorch); !ok { + t.Fatal("expects a scorch index") + } + + // no merge operations are expected as per the original merge policy. + nfsr := atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot) + if nfsr != 20 { + t.Errorf("expected 20 root file segments, got: %d", nfsr) + } + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + // cancel the force merge operation once the root has some new merge + // introductions. ie if the root has lesser file segments than earlier. + go func() { + for { + nval := atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot) + if nval < nfsr { + cancel() + return + } + time.Sleep(time.Millisecond * 5) + } + }() + + err = si.ForceMerge(ctx, &mergeplan.MergePlanOptions{ + MaxSegmentsPerTier: 1, + MaxSegmentSize: 10000, + SegmentsPerMergeTask: 5, + FloorSegmentSize: 10000, + }) + if err != nil { + t.Errorf("ForceMerge failed, err: %v", err) + } + + // verify the final root file segment count or forceMerge completion + if atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot) == 1 { + t.Errorf("expected many files at root, but got: %d segments", + atomic.LoadUint64(&si.stats.TotFileSegmentsAtRoot)) + } + + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexSeekBackwardsStats(t *testing.T) { + cfg := CreateConfig("TestIndexOpenReopen") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // insert a doc + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("cat"))) + err = idx.Update(doc) + if err != nil { + t.Fatalf("error updating index: %v", err) + } + + // insert another doc + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("cat"))) + err = idx.Update(doc) + if err != nil { + t.Fatalf("error updating index: %v", err) + } + + reader, err := idx.Reader() + if err != nil { + t.Fatalf("error getting index reader: %v", err) + } + defer reader.Close() + + tfr, err := reader.TermFieldReader(context.TODO(), []byte("cat"), "name", false, false, false) + if err != nil { + t.Fatalf("error getting term field readyer for name/cat: %v", err) + } + + tfdFirst, err := tfr.Next(nil) + if err != nil { + t.Fatalf("error getting first tfd: %v", err) + } + + _, err = tfr.Next(nil) + if err != nil { + t.Fatalf("error getting second tfd: %v", err) + } + + // seek backwards to the first + _, err = tfr.Advance(tfdFirst.ID, nil) + if err != nil { + t.Fatalf("error adancing backwards: %v", err) + } + + err = tfr.Close() + if err != nil { + t.Fatalf("error closing term field reader: %v", err) + } + + if idx.(*Scorch).stats.TotTermSearchersStarted != idx.(*Scorch).stats.TotTermSearchersFinished { + t.Errorf("expected term searchers started %d to equal term searchers finished %d", + idx.(*Scorch).stats.TotTermSearchersStarted, + idx.(*Scorch).stats.TotTermSearchersFinished) + } +} + +// fieldTerms contains the terms used by a document, keyed by field +type fieldTerms map[string][]string + +// FieldsNotYetCached returns a list of fields not yet cached out of a larger list of fields +func (f fieldTerms) FieldsNotYetCached(fields []string) []string { + rv := make([]string, 0, len(fields)) + for _, field := range fields { + if _, ok := f[field]; !ok { + rv = append(rv, field) + } + } + return rv +} + +// Merge will combine two fieldTerms +// it assumes that the terms lists are complete (thus do not need to be merged) +// field terms from the other list always replace the ones in the receiver +func (f fieldTerms) Merge(other fieldTerms) { + for field, terms := range other { + f[field] = terms + } +} + +func TestOpenBoltTimeout(t *testing.T) { + cfg := CreateConfig("TestIndexOpenReopen") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewScorch("storeName", cfg, analysisQueue) + if err != nil { + log.Fatalln(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + // new config + cfg2 := CreateConfig("TestIndexOpenReopen") + // copy path from original config + cfg2["path"] = cfg["path"] + // set timeout in this cfg + cfg2["bolt_timeout"] = "100ms" + + idx2, err := NewScorch("storeName", cfg2, analysisQueue) + if err != nil { + log.Fatalln(err) + } + err = idx2.Open() + if err == nil { + t.Error("expected timeout error opening index again") + } +} + +func TestReadOnlyIndex(t *testing.T) { + // https://github.com/blevesearch/bleve/issues/1623 + cfg := CreateConfig("TestReadOnlyIndex") + err := InitTest(cfg) + if err != nil { + t.Fatal(err) + } + defer func() { + err := DestroyTest(cfg) + if err != nil { + t.Log(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + writeIdx, err := NewScorch(Name, cfg, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = writeIdx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + writeIdxClosed := false + defer func() { + if !writeIdxClosed { + err := writeIdx.Close() + if err != nil { + t.Fatal(err) + } + } + }() + + // Add a single document to the index. + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = writeIdx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + writeIdx.Close() + writeIdxClosed = true + + // After the index is written, change permissions on every file + // in the index to read-only. + var permissionsFunc func(folder string) + permissionsFunc = func(folder string) { + entries, _ := os.ReadDir(folder) + for _, entry := range entries { + fullName := filepath.Join(folder, entry.Name()) + if entry.IsDir() { + permissionsFunc(fullName) + } else { + if err := os.Chmod(fullName, 0o555); err != nil { + t.Fatal(err) + } + } + } + } + permissionsFunc(cfg["path"].(string)) + + // Now reopen the index in read-only mode and attempt to read from it. + cfg["read_only"] = true + readIdx, err := NewScorch(Name, cfg, analysisQueue) + defer func() { + err := readIdx.Close() + if err != nil { + t.Fatal(err) + } + }() + + if err != nil { + t.Fatal(err) + } + err = readIdx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + reader, err := readIdx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Fatal(err) + } + if docCount != 1 { + t.Errorf("Expected document count to be %d got %d", 1, docCount) + } +} + +func BenchmarkAggregateFieldStats(b *testing.B) { + fieldStatsArray := make([]*fieldStats, 1000) + + for i := range fieldStatsArray { + fieldStatsArray[i] = newFieldStats() + + fieldStatsArray[i].Store("num_vectors", "vector", uint64(rand.Intn(1000))) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + aggFieldStats := newFieldStats() + + for _, fs := range fieldStatsArray { + aggFieldStats.Aggregate(fs) + } + } +} + +func TestPersistorMergerOptions(t *testing.T) { + type test struct { + config string + expectErr bool + } + tests := []test{ + { + // valid config and no error expected + config: `{ + "scorchPersisterOptions": { + "persisterNapTimeMSec": 1110, + "memoryPressurePauseThreshold" : 333 + } + }`, + expectErr: false, + }, + { + // valid json with invalid config values + // and error expected + config: `{ + "scorchPersisterOptions": { + "persisterNapTimeMSec": "1110", + "memoryPressurePauseThreshold" : [333] + } + }`, + expectErr: true, + }, + { + // valid json with invalid config values + // and error expected + config: `{ + "scorchPersisterOptions": { + "persisterNapTimeMSec": 1110.2, + "memoryPressurePauseThreshold" : 333 + } + }`, + expectErr: true, + }, + { + // invalid setting for scorchMergePlanOptions + config: `{ + "scorchPersisterOptions": { + "persisterNapTimeMSec": 1110, + "memoryPressurePauseThreshold" : 333 + }, + "scorchMergePlanOptions": [{ + "maxSegmentSize": 10000, + "maxSegmentsPerTier": 10, + "segmentsPerMergeTask": 10 + }] + }`, + expectErr: true, + }, + { + // valid setting + config: `{ + "scorchPersisterOptions": { + "persisterNapTimeMSec": 1110, + "memoryPressurePauseThreshold" : 333 + }, + "scorchMergePlanOptions": { + "maxSegmentSize": 10000, + "maxSegmentsPerTier": 10, + "segmentsPerMergeTask": 10 + } + }`, + expectErr: false, + }, + { + config: `{ + "scorchPersisterOptions": { + "persisterNapTimeMSec": 1110, + "memoryPressurePauseThreshold" : 333 + }, + "scorchMergePlanOptions": { + "maxSegmentSize": 5.6, + "maxSegmentsPerTier": 10, + "segmentsPerMergeTask": 10 + } + }`, + expectErr: true, + }, + } + for i, test := range tests { + cfg := map[string]interface{}{} + err := json.Unmarshal([]byte(test.config), &cfg) + if err != nil { + t.Fatalf("test %d: error unmarshalling config: %v", i, err) + } + analysisQueue := index.NewAnalysisQueue(1) + _, err = NewScorch(Name, cfg, analysisQueue) + if test.expectErr { + if err == nil { + t.Errorf("test %d: expected error, got nil", i) + } + } else { + if err != nil { + t.Errorf("test %d: unexpected error: %v", i, err) + } + } + } +} diff --git a/index/scorch/segment_plugin.go b/index/scorch/segment_plugin.go new file mode 100644 index 0000000..790a800 --- /dev/null +++ b/index/scorch/segment_plugin.go @@ -0,0 +1,144 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "fmt" + + "github.com/RoaringBitmap/roaring/v2" + "github.com/blevesearch/bleve/v2/geo" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" + + zapv11 "github.com/blevesearch/zapx/v11" + zapv12 "github.com/blevesearch/zapx/v12" + zapv13 "github.com/blevesearch/zapx/v13" + zapv14 "github.com/blevesearch/zapx/v14" + zapv15 "github.com/blevesearch/zapx/v15" + zapv16 "github.com/blevesearch/zapx/v16" +) + +// SegmentPlugin represents the essential functions required by a package to plug in +// it's segment implementation +type SegmentPlugin interface { + + // Type is the name for this segment plugin + Type() string + + // Version is a numeric value identifying a specific version of this type. + // When incompatible changes are made to a particular type of plugin, the + // version must be incremented. + Version() uint32 + + // New takes a set of Documents and turns them into a new Segment + New(results []index.Document) (segment.Segment, uint64, error) + + // Open attempts to open the file at the specified path and + // return the corresponding Segment + Open(path string) (segment.Segment, error) + + // Merge takes a set of Segments, and creates a new segment on disk at + // the specified path. + // Drops is a set of bitmaps (one for each segment) indicating which + // documents can be dropped from the segments during the merge. + // If the closeCh channel is closed, Merge will cease doing work at + // the next opportunity, and return an error (closed). + // StatsReporter can optionally be provided, in which case progress + // made during the merge is reported while operation continues. + // Returns: + // A slice of new document numbers (one for each input segment), + // this allows the caller to know a particular document's new + // document number in the newly merged segment. + // The number of bytes written to the new segment file. + // An error, if any occurred. + Merge(segments []segment.Segment, drops []*roaring.Bitmap, path string, + closeCh chan struct{}, s segment.StatsReporter) ( + [][]uint64, uint64, error) +} + +var supportedSegmentPlugins map[string]map[uint32]SegmentPlugin +var defaultSegmentPlugin SegmentPlugin + +func init() { + ResetSegmentPlugins() + RegisterSegmentPlugin(&zapv16.ZapPlugin{}, true) + RegisterSegmentPlugin(&zapv15.ZapPlugin{}, false) + RegisterSegmentPlugin(&zapv14.ZapPlugin{}, false) + RegisterSegmentPlugin(&zapv13.ZapPlugin{}, false) + RegisterSegmentPlugin(&zapv12.ZapPlugin{}, false) + RegisterSegmentPlugin(&zapv11.ZapPlugin{}, false) +} + +func ResetSegmentPlugins() { + supportedSegmentPlugins = map[string]map[uint32]SegmentPlugin{} +} + +func RegisterSegmentPlugin(plugin SegmentPlugin, makeDefault bool) { + if _, ok := supportedSegmentPlugins[plugin.Type()]; !ok { + supportedSegmentPlugins[plugin.Type()] = map[uint32]SegmentPlugin{} + } + supportedSegmentPlugins[plugin.Type()][plugin.Version()] = plugin + if makeDefault { + defaultSegmentPlugin = plugin + } +} + +func SupportedSegmentTypes() (rv []string) { + for k := range supportedSegmentPlugins { + rv = append(rv, k) + } + return +} + +func SupportedSegmentTypeVersions(typ string) (rv []uint32) { + for k := range supportedSegmentPlugins[typ] { + rv = append(rv, k) + } + return rv +} + +func chooseSegmentPlugin(forcedSegmentType string, + forcedSegmentVersion uint32) (SegmentPlugin, error) { + if versions, ok := supportedSegmentPlugins[forcedSegmentType]; ok { + if segPlugin, ok := versions[uint32(forcedSegmentVersion)]; ok { + return segPlugin, nil + } + return nil, fmt.Errorf( + "unsupported version %d for segment type: %s, supported: %v", + forcedSegmentVersion, forcedSegmentType, + SupportedSegmentTypeVersions(forcedSegmentType)) + } + return nil, fmt.Errorf("unsupported segment type: %s, supported: %v", + forcedSegmentType, SupportedSegmentTypes()) +} + +func (s *Scorch) loadSegmentPlugin(forcedSegmentType string, + forcedSegmentVersion uint32) error { + segPlugin, err := chooseSegmentPlugin(forcedSegmentType, + forcedSegmentVersion) + if err != nil { + return err + } + s.segPlugin = segPlugin + return nil +} + +func (s *Scorch) loadSpatialAnalyzerPlugin(typ string) error { + s.spatialPlugin = geo.GetSpatialAnalyzerPlugin(typ) + if s.spatialPlugin == nil { + return fmt.Errorf("unsupported spatial plugin type: %s", typ) + } + return nil +} diff --git a/index/scorch/snapshot_index.go b/index/scorch/snapshot_index.go new file mode 100644 index 0000000..4f67a3c --- /dev/null +++ b/index/scorch/snapshot_index.go @@ -0,0 +1,1163 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "container/heap" + "context" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "reflect" + "sort" + "sync" + "sync/atomic" + + "github.com/RoaringBitmap/roaring/v2" + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" + "github.com/blevesearch/vellum" + lev "github.com/blevesearch/vellum/levenshtein" + bolt "go.etcd.io/bbolt" +) + +// re usable, threadsafe levenshtein builders +var lb1, lb2 *lev.LevenshteinAutomatonBuilder + +type asynchSegmentResult struct { + dict segment.TermDictionary + dictItr segment.DictionaryIterator + + cardinality int + index int + docs *roaring.Bitmap + + thesItr segment.ThesaurusIterator + + err error +} + +var reflectStaticSizeIndexSnapshot int + +func init() { + var is interface{} = IndexSnapshot{} + reflectStaticSizeIndexSnapshot = int(reflect.TypeOf(is).Size()) + var err error + lb1, err = lev.NewLevenshteinAutomatonBuilder(1, true) + if err != nil { + panic(fmt.Errorf("Levenshtein automaton ed1 builder err: %v", err)) + } + lb2, err = lev.NewLevenshteinAutomatonBuilder(2, true) + if err != nil { + panic(fmt.Errorf("Levenshtein automaton ed2 builder err: %v", err)) + } +} + +type IndexSnapshot struct { + parent *Scorch + segment []*SegmentSnapshot + offsets []uint64 + internal map[string][]byte + epoch uint64 + size uint64 + creator string + + m sync.Mutex // Protects the fields that follow. + refs int64 + + m2 sync.Mutex // Protects the fields that follow. + fieldTFRs map[string][]*IndexSnapshotTermFieldReader // keyed by field, recycled TFR's + + m3 sync.RWMutex // bm25 metrics specific - not to interfere with TFR creation + fieldCardinality map[string]int +} + +func (i *IndexSnapshot) Segments() []*SegmentSnapshot { + return i.segment +} + +func (i *IndexSnapshot) Internal() map[string][]byte { + return i.internal +} + +func (i *IndexSnapshot) AddRef() { + i.m.Lock() + i.refs++ + i.m.Unlock() +} + +func (i *IndexSnapshot) DecRef() (err error) { + i.m.Lock() + i.refs-- + if i.refs == 0 { + for _, s := range i.segment { + if s != nil { + err2 := s.segment.DecRef() + if err == nil { + err = err2 + } + } + } + if i.parent != nil { + go i.parent.AddEligibleForRemoval(i.epoch) + } + } + i.m.Unlock() + return err +} + +func (i *IndexSnapshot) Close() error { + return i.DecRef() +} + +func (i *IndexSnapshot) Size() int { + return int(i.size) +} + +func (i *IndexSnapshot) updateSize() { + i.size += uint64(reflectStaticSizeIndexSnapshot) + for _, s := range i.segment { + i.size += uint64(s.Size()) + } +} + +func (is *IndexSnapshot) newIndexSnapshotFieldDict(field string, + makeItr func(i segment.TermDictionary) segment.DictionaryIterator, + randomLookup bool, +) (*IndexSnapshotFieldDict, error) { + results := make(chan *asynchSegmentResult) + var totalBytesRead uint64 + var fieldCardinality int64 + for _, s := range is.segment { + go func(s *SegmentSnapshot) { + dict, err := s.segment.Dictionary(field) + if err != nil { + results <- &asynchSegmentResult{err: err} + } else { + if dictStats, ok := dict.(segment.DiskStatsReporter); ok { + atomic.AddUint64(&totalBytesRead, dictStats.BytesRead()) + } + atomic.AddInt64(&fieldCardinality, int64(dict.Cardinality())) + if randomLookup { + results <- &asynchSegmentResult{dict: dict} + } else { + results <- &asynchSegmentResult{dictItr: makeItr(dict)} + } + } + }(s) + } + + var err error + rv := &IndexSnapshotFieldDict{ + snapshot: is, + cursors: make([]*segmentDictCursor, 0, len(is.segment)), + } + + for count := 0; count < len(is.segment); count++ { + asr := <-results + if asr.err != nil && err == nil { + err = asr.err + } else { + if !randomLookup { + next, err2 := asr.dictItr.Next() + if err2 != nil && err == nil { + err = err2 + } + if next != nil { + rv.cursors = append(rv.cursors, &segmentDictCursor{ + itr: asr.dictItr, + curr: *next, + }) + } + } else { + rv.cursors = append(rv.cursors, &segmentDictCursor{ + dict: asr.dict, + }) + } + } + } + rv.cardinality = int(fieldCardinality) + rv.bytesRead = totalBytesRead + // after ensuring we've read all items on channel + if err != nil { + return nil, err + } + + if !randomLookup { + // prepare heap + heap.Init(rv) + } + + return rv, nil +} + +func (is *IndexSnapshot) FieldCardinality(field string) (rv int, err error) { + is.m3.RLock() + rv, ok := is.fieldCardinality[field] + is.m3.RUnlock() + if ok { + return rv, nil + } + + is.m3.Lock() + defer is.m3.Unlock() + if is.fieldCardinality == nil { + is.fieldCardinality = make(map[string]int) + } + // check again to avoid redundant fieldDict creation + if rv, ok := is.fieldCardinality[field]; ok { + return rv, nil + } + + fd, err := is.FieldDict(field) + if err != nil { + return rv, err + } + rv = fd.Cardinality() + is.fieldCardinality[field] = rv + return rv, nil +} + +func (is *IndexSnapshot) FieldDict(field string) (index.FieldDict, error) { + return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator { + return is.AutomatonIterator(nil, nil, nil) + }, false) +} + +// calculateExclusiveEndFromInclusiveEnd produces the next key +// when sorting using memcmp style comparisons, suitable to +// use as the end key in a traditional (inclusive, exclusive] +// start/end range +func calculateExclusiveEndFromInclusiveEnd(inclusiveEnd []byte) []byte { + rv := inclusiveEnd + if len(inclusiveEnd) > 0 { + rv = make([]byte, len(inclusiveEnd)) + copy(rv, inclusiveEnd) + if rv[len(rv)-1] < 0xff { + // last byte can be incremented by one + rv[len(rv)-1]++ + } else { + // last byte is already 0xff, so append 0 + // next key is simply one byte longer + rv = append(rv, 0x0) + } + } + return rv +} + +func (is *IndexSnapshot) FieldDictRange(field string, startTerm []byte, + endTerm []byte, +) (index.FieldDict, error) { + return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator { + endTermExclusive := calculateExclusiveEndFromInclusiveEnd(endTerm) + return is.AutomatonIterator(nil, startTerm, endTermExclusive) + }, false) +} + +// calculateExclusiveEndFromPrefix produces the first key that +// does not have the same prefix as the input bytes, suitable +// to use as the end key in a traditional (inclusive, exclusive] +// start/end range +func calculateExclusiveEndFromPrefix(in []byte) []byte { + rv := make([]byte, len(in)) + copy(rv, in) + for i := len(rv) - 1; i >= 0; i-- { + rv[i] = rv[i] + 1 + if rv[i] != 0 { + return rv // didn't overflow, so stop + } + } + // all bytes were 0xff, so return nil + // as there is no end key for this prefix + return nil +} + +func (is *IndexSnapshot) FieldDictPrefix(field string, + termPrefix []byte, +) (index.FieldDict, error) { + termPrefixEnd := calculateExclusiveEndFromPrefix(termPrefix) + return is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator { + return is.AutomatonIterator(nil, termPrefix, termPrefixEnd) + }, false) +} + +func (is *IndexSnapshot) FieldDictRegexp(field string, + termRegex string, +) (index.FieldDict, error) { + fd, _, err := is.FieldDictRegexpAutomaton(field, termRegex) + return fd, err +} + +func (is *IndexSnapshot) FieldDictRegexpAutomaton(field string, + termRegex string, +) (index.FieldDict, index.RegexAutomaton, error) { + return is.fieldDictRegexp(field, termRegex) +} + +func (is *IndexSnapshot) fieldDictRegexp(field string, + termRegex string, +) (index.FieldDict, index.RegexAutomaton, error) { + // TODO: potential optimization where the literal prefix represents the, + // entire regexp, allowing us to use PrefixIterator(prefixTerm)? + + a, prefixBeg, prefixEnd, err := parseRegexp(termRegex) + if err != nil { + return nil, nil, err + } + + fd, err := is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator { + return is.AutomatonIterator(a, prefixBeg, prefixEnd) + }, false) + if err != nil { + return nil, nil, err + } + return fd, a, nil +} + +func (is *IndexSnapshot) getLevAutomaton(term string, + fuzziness uint8, +) (vellum.Automaton, error) { + switch fuzziness { + case 1: + return lb1.BuildDfa(term, fuzziness) + case 2: + return lb2.BuildDfa(term, fuzziness) + } + return nil, fmt.Errorf("fuzziness exceeds the max limit") +} + +func (is *IndexSnapshot) FieldDictFuzzy(field string, + term string, fuzziness int, prefix string, +) (index.FieldDict, error) { + fd, _, err := is.FieldDictFuzzyAutomaton(field, term, fuzziness, prefix) + return fd, err +} + +func (is *IndexSnapshot) FieldDictFuzzyAutomaton(field string, + term string, fuzziness int, prefix string, +) (index.FieldDict, index.FuzzyAutomaton, error) { + return is.fieldDictFuzzy(field, term, fuzziness, prefix) +} + +func (is *IndexSnapshot) fieldDictFuzzy(field string, + term string, fuzziness int, prefix string, +) (index.FieldDict, index.FuzzyAutomaton, error) { + a, err := is.getLevAutomaton(term, uint8(fuzziness)) + if err != nil { + return nil, nil, err + } + var fa index.FuzzyAutomaton + if vfa, ok := a.(vellum.FuzzyAutomaton); ok { + fa = vfa + } + var prefixBeg, prefixEnd []byte + if prefix != "" { + prefixBeg = []byte(prefix) + prefixEnd = calculateExclusiveEndFromPrefix(prefixBeg) + } + fd, err := is.newIndexSnapshotFieldDict(field, func(is segment.TermDictionary) segment.DictionaryIterator { + return is.AutomatonIterator(a, prefixBeg, prefixEnd) + }, false) + if err != nil { + return nil, nil, err + } + return fd, fa, nil +} + +func (is *IndexSnapshot) FieldDictContains(field string) (index.FieldDictContains, error) { + return is.newIndexSnapshotFieldDict(field, nil, true) +} + +func (is *IndexSnapshot) DocIDReaderAll() (index.DocIDReader, error) { + results := make(chan *asynchSegmentResult) + for index, segment := range is.segment { + go func(index int, segment *SegmentSnapshot) { + results <- &asynchSegmentResult{ + index: index, + docs: segment.DocNumbersLive(), + } + }(index, segment) + } + + return is.newDocIDReader(results) +} + +func (is *IndexSnapshot) DocIDReaderOnly(ids []string) (index.DocIDReader, error) { + results := make(chan *asynchSegmentResult) + for index, segment := range is.segment { + go func(index int, segment *SegmentSnapshot) { + docs, err := segment.DocNumbers(ids) + if err != nil { + results <- &asynchSegmentResult{err: err} + } else { + results <- &asynchSegmentResult{ + index: index, + docs: docs, + } + } + }(index, segment) + } + + return is.newDocIDReader(results) +} + +func (is *IndexSnapshot) newDocIDReader(results chan *asynchSegmentResult) (index.DocIDReader, error) { + rv := &IndexSnapshotDocIDReader{ + snapshot: is, + iterators: make([]roaring.IntIterable, len(is.segment)), + } + var err error + for count := 0; count < len(is.segment); count++ { + asr := <-results + if asr.err != nil { + if err == nil { + // returns the first error encountered + err = asr.err + } + } else if err == nil { + rv.iterators[asr.index] = asr.docs.Iterator() + } + } + + if err != nil { + return nil, err + } + + return rv, nil +} + +func (is *IndexSnapshot) Fields() ([]string, error) { + // FIXME not making this concurrent for now as it's not used in hot path + // of any searches at the moment (just a debug aid) + fieldsMap := map[string]struct{}{} + for _, segment := range is.segment { + fields := segment.Fields() + for _, field := range fields { + fieldsMap[field] = struct{}{} + } + } + rv := make([]string, 0, len(fieldsMap)) + for k := range fieldsMap { + rv = append(rv, k) + } + return rv, nil +} + +func (is *IndexSnapshot) GetInternal(key []byte) ([]byte, error) { + return is.internal[string(key)], nil +} + +func (is *IndexSnapshot) DocCount() (uint64, error) { + var rv uint64 + for _, segment := range is.segment { + rv += segment.Count() + } + return rv, nil +} + +func (is *IndexSnapshot) Document(id string) (rv index.Document, err error) { + // FIXME could be done more efficiently directly, but reusing for simplicity + tfr, err := is.TermFieldReader(context.TODO(), []byte(id), "_id", false, false, false) + if err != nil { + return nil, err + } + defer func() { + if cerr := tfr.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + next, err := tfr.Next(nil) + if err != nil { + return nil, err + } + + if next == nil { + // no such doc exists + return nil, nil + } + + docNum, err := docInternalToNumber(next.ID) + if err != nil { + return nil, err + } + segmentIndex, localDocNum := is.segmentIndexAndLocalDocNumFromGlobal(docNum) + + rvd := document.NewDocument(id) + + err = is.segment[segmentIndex].VisitDocument(localDocNum, func(name string, typ byte, val []byte, pos []uint64) bool { + if name == "_id" { + return true + } + + // track uncompressed stored fields bytes as part of IO stats. + // However, ideally we'd need to track the compressed on-disk value + // Keeping that TODO for now until we have a cleaner way. + rvd.StoredFieldsSize += uint64(len(val)) + + // copy value, array positions to preserve them beyond the scope of this callback + value := append([]byte(nil), val...) + arrayPos := append([]uint64(nil), pos...) + + switch typ { + case 't': + rvd.AddField(document.NewTextField(name, arrayPos, value)) + case 'n': + rvd.AddField(document.NewNumericFieldFromBytes(name, arrayPos, value)) + case 'i': + rvd.AddField(document.NewIPFieldFromBytes(name, arrayPos, value)) + case 'd': + rvd.AddField(document.NewDateTimeFieldFromBytes(name, arrayPos, value)) + case 'b': + rvd.AddField(document.NewBooleanFieldFromBytes(name, arrayPos, value)) + case 'g': + rvd.AddField(document.NewGeoPointFieldFromBytes(name, arrayPos, value)) + case 's': + rvd.AddField(document.NewGeoShapeFieldFromBytes(name, arrayPos, value)) + } + + return true + }) + if err != nil { + return nil, err + } + + return rvd, nil +} + +// In a multi-segment index, each document has: +// 1. a local docnum - local to the segment +// 2. a global docnum - unique identifier across the index +// This function returns the segment index(the segment in which the docnum is present) +// and local docnum of a document. +func (is *IndexSnapshot) segmentIndexAndLocalDocNumFromGlobal(docNum uint64) (int, uint64) { + segmentIndex := sort.Search(len(is.offsets), + func(x int) bool { + return is.offsets[x] > docNum + }) - 1 + + return int(segmentIndex), docNum - is.offsets[segmentIndex] +} + +func (is *IndexSnapshot) ExternalID(id index.IndexInternalID) (string, error) { + docNum, err := docInternalToNumber(id) + if err != nil { + return "", err + } + segmentIndex, localDocNum := is.segmentIndexAndLocalDocNumFromGlobal(docNum) + + v, err := is.segment[segmentIndex].DocID(localDocNum) + if err != nil { + return "", err + } + if v == nil { + return "", fmt.Errorf("document number %d not found", docNum) + } + + return string(v), nil +} + +func (is *IndexSnapshot) segmentIndexAndLocalDocNum(id index.IndexInternalID) (int, uint64, error) { + docNum, err := docInternalToNumber(id) + if err != nil { + return 0, 0, err + } + segIdx, localDocNum := is.segmentIndexAndLocalDocNumFromGlobal(docNum) + return segIdx, localDocNum, nil +} + +func (is *IndexSnapshot) InternalID(id string) (rv index.IndexInternalID, err error) { + // FIXME could be done more efficiently directly, but reusing for simplicity + tfr, err := is.TermFieldReader(context.TODO(), []byte(id), "_id", false, false, false) + if err != nil { + return nil, err + } + defer func() { + if cerr := tfr.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + next, err := tfr.Next(nil) + if err != nil || next == nil { + return nil, err + } + + return next.ID, nil +} + +func (is *IndexSnapshot) TermFieldReader(ctx context.Context, term []byte, field string, includeFreq, + includeNorm, includeTermVectors bool, +) (index.TermFieldReader, error) { + rv := is.allocTermFieldReaderDicts(field) + + rv.ctx = ctx + rv.term = term + rv.field = field + rv.snapshot = is + if rv.postings == nil { + rv.postings = make([]segment.PostingsList, len(is.segment)) + } + if rv.iterators == nil { + rv.iterators = make([]segment.PostingsIterator, len(is.segment)) + } + rv.segmentOffset = 0 + rv.includeFreq = includeFreq + rv.includeNorm = includeNorm + rv.includeTermVectors = includeTermVectors + rv.currPosting = nil + rv.currID = rv.currID[:0] + + if rv.dicts == nil { + rv.dicts = make([]segment.TermDictionary, len(is.segment)) + for i, s := range is.segment { + // the intention behind this compare and swap operation is + // to make sure that the accounting of the metadata is happening + // only once(which corresponds to this persisted segment's most + // recent segPlugin.Open() call), and any subsequent queries won't + // incur this cost which would essentially be a double counting. + if atomic.CompareAndSwapUint32(&s.mmaped, 1, 0) { + segBytesRead := s.segment.BytesRead() + rv.incrementBytesRead(segBytesRead) + } + dict, err := s.segment.Dictionary(field) + if err != nil { + return nil, err + } + if dictStats, ok := dict.(segment.DiskStatsReporter); ok { + bytesRead := dictStats.BytesRead() + rv.incrementBytesRead(bytesRead) + } + rv.dicts[i] = dict + } + } + + for i, s := range is.segment { + var prevBytesReadPL uint64 + if rv.postings[i] != nil { + prevBytesReadPL = rv.postings[i].BytesRead() + } + pl, err := rv.dicts[i].PostingsList(term, s.deleted, rv.postings[i]) + if err != nil { + return nil, err + } + rv.postings[i] = pl + + var prevBytesReadItr uint64 + if rv.iterators[i] != nil { + prevBytesReadItr = rv.iterators[i].BytesRead() + } + rv.iterators[i] = pl.Iterator(includeFreq, includeNorm, includeTermVectors, rv.iterators[i]) + + if bytesRead := rv.postings[i].BytesRead(); prevBytesReadPL < bytesRead { + rv.incrementBytesRead(bytesRead - prevBytesReadPL) + } + + if bytesRead := rv.iterators[i].BytesRead(); prevBytesReadItr < bytesRead { + rv.incrementBytesRead(bytesRead - prevBytesReadItr) + } + } + atomic.AddUint64(&is.parent.stats.TotTermSearchersStarted, uint64(1)) + return rv, nil +} + +func (is *IndexSnapshot) allocTermFieldReaderDicts(field string) (tfr *IndexSnapshotTermFieldReader) { + is.m2.Lock() + if is.fieldTFRs != nil { + tfrs := is.fieldTFRs[field] + last := len(tfrs) - 1 + if last >= 0 { + tfr = tfrs[last] + tfrs[last] = nil + is.fieldTFRs[field] = tfrs[:last] + is.m2.Unlock() + return + } + } + is.m2.Unlock() + return &IndexSnapshotTermFieldReader{ + recycle: true, + } +} + +// DefaultFieldTFRCacheThreshold limits the number of TermFieldReaders(TFR) for +// a field in an index snapshot. Without this limit, when recycling TFRs, it is +// possible that a very large number of TFRs may be added to the recycle +// cache, which could eventually lead to significant memory consumption. +// This threshold can be overwritten by users at the library level by changing the +// exported variable, or at the index level by setting the "fieldTFRCacheThreshold" +// in the kvConfig. +var DefaultFieldTFRCacheThreshold int = 0 // disabled because it causes MB-64604 + +func (is *IndexSnapshot) getFieldTFRCacheThreshold() int { + if is.parent.config != nil { + if val, exists := is.parent.config["fieldTFRCacheThreshold"]; exists { + if x, ok := val.(float64); ok { + // JSON unmarshal-ed into a map[string]interface{} will default + // to float64 for numbers, so we need to check for float64 first. + return int(x) + } else if x, ok := val.(int); ok { + // If library users provided an int in the config, we'll honor it. + return x + } + } + } + return DefaultFieldTFRCacheThreshold +} + +func (is *IndexSnapshot) recycleTermFieldReader(tfr *IndexSnapshotTermFieldReader) { + if !tfr.recycle { + // Do not recycle an optimized unadorned term field reader (used for + // ConjunctionUnadorned or DisjunctionUnadorned), during when a fresh + // roaring.Bitmap is built by AND-ing or OR-ing individual bitmaps, + // and we'll need to release them for GC. (See MB-40916) + return + } + + is.parent.rootLock.RLock() + obsolete := is.parent.root != is + is.parent.rootLock.RUnlock() + if obsolete { + // if we're not the current root (mutations happened), don't bother recycling + return + } + + is.m2.Lock() + if is.fieldTFRs == nil { + is.fieldTFRs = map[string][]*IndexSnapshotTermFieldReader{} + } + if len(is.fieldTFRs[tfr.field]) < is.getFieldTFRCacheThreshold() { + tfr.bytesRead = 0 + is.fieldTFRs[tfr.field] = append(is.fieldTFRs[tfr.field], tfr) + } + is.m2.Unlock() +} + +func docNumberToBytes(buf []byte, in uint64) []byte { + if len(buf) != 8 { + if cap(buf) >= 8 { + buf = buf[0:8] + } else { + buf = make([]byte, 8) + } + } + binary.BigEndian.PutUint64(buf, in) + return buf +} + +func docInternalToNumber(in index.IndexInternalID) (uint64, error) { + if len(in) != 8 { + return 0, fmt.Errorf("wrong len for IndexInternalID: %q", in) + } + return binary.BigEndian.Uint64(in), nil +} + +func (is *IndexSnapshot) documentVisitFieldTermsOnSegment( + segmentIndex int, localDocNum uint64, fields []string, cFields []string, + visitor index.DocValueVisitor, dvs segment.DocVisitState) ( + cFieldsOut []string, dvsOut segment.DocVisitState, err error, +) { + ss := is.segment[segmentIndex] + + var vFields []string // fields that are visitable via the segment + + ssv, ssvOk := ss.segment.(segment.DocValueVisitable) + if ssvOk && ssv != nil { + vFields, err = ssv.VisitableDocValueFields() + if err != nil { + return nil, nil, err + } + } + + var errCh chan error + + // cFields represents the fields that we'll need from the + // cachedDocs, and might be optionally be provided by the caller, + // if the caller happens to know we're on the same segmentIndex + // from a previous invocation + if cFields == nil { + cFields = subtractStrings(fields, vFields) + + if !ss.cachedDocs.hasFields(cFields) { + errCh = make(chan error, 1) + + go func() { + err := ss.cachedDocs.prepareFields(cFields, ss) + if err != nil { + errCh <- err + } + close(errCh) + }() + } + } + + if ssvOk && ssv != nil && len(vFields) > 0 { + dvs, err = ssv.VisitDocValues(localDocNum, fields, visitor, dvs) + if err != nil { + return nil, nil, err + } + } + + if errCh != nil { + err = <-errCh + if err != nil { + return nil, nil, err + } + } + + if len(cFields) > 0 { + ss.cachedDocs.visitDoc(localDocNum, cFields, visitor) + } + + return cFields, dvs, nil +} + +func (is *IndexSnapshot) DocValueReader(fields []string) ( + index.DocValueReader, error, +) { + return &DocValueReader{i: is, fields: fields, currSegmentIndex: -1}, nil +} + +type DocValueReader struct { + i *IndexSnapshot + fields []string + dvs segment.DocVisitState + + currSegmentIndex int + currCachedFields []string + + totalBytesRead uint64 + bytesRead uint64 +} + +func (dvr *DocValueReader) BytesRead() uint64 { + return dvr.totalBytesRead + dvr.bytesRead +} + +func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID, + visitor index.DocValueVisitor, +) (err error) { + docNum, err := docInternalToNumber(id) + if err != nil { + return err + } + + segmentIndex, localDocNum := dvr.i.segmentIndexAndLocalDocNumFromGlobal(docNum) + if segmentIndex >= len(dvr.i.segment) { + return nil + } + + if dvr.currSegmentIndex != segmentIndex { + dvr.currSegmentIndex = segmentIndex + dvr.currCachedFields = nil + dvr.totalBytesRead += dvr.bytesRead + dvr.bytesRead = 0 + } + + dvr.currCachedFields, dvr.dvs, err = dvr.i.documentVisitFieldTermsOnSegment( + dvr.currSegmentIndex, localDocNum, dvr.fields, dvr.currCachedFields, visitor, dvr.dvs) + + if dvr.dvs != nil { + dvr.bytesRead = dvr.dvs.BytesRead() + } + return err +} + +func (is *IndexSnapshot) DumpAll() chan interface{} { + rv := make(chan interface{}) + go func() { + close(rv) + }() + return rv +} + +func (is *IndexSnapshot) DumpDoc(id string) chan interface{} { + rv := make(chan interface{}) + go func() { + close(rv) + }() + return rv +} + +func (is *IndexSnapshot) DumpFields() chan interface{} { + rv := make(chan interface{}) + go func() { + close(rv) + }() + return rv +} + +func (is *IndexSnapshot) diskSegmentsPaths() map[string]struct{} { + rv := make(map[string]struct{}, len(is.segment)) + for _, s := range is.segment { + if seg, ok := s.segment.(segment.PersistedSegment); ok { + rv[seg.Path()] = struct{}{} + } + } + return rv +} + +// reClaimableDocsRatio gives a ratio about the obsoleted or +// reclaimable documents present in a given index snapshot. +func (is *IndexSnapshot) reClaimableDocsRatio() float64 { + var totalCount, liveCount uint64 + for _, s := range is.segment { + if _, ok := s.segment.(segment.PersistedSegment); ok { + totalCount += uint64(s.FullSize()) + liveCount += uint64(s.Count()) + } + } + + if totalCount > 0 { + return float64(totalCount-liveCount) / float64(totalCount) + } + return 0 +} + +// subtractStrings returns set a minus elements of set b. +func subtractStrings(a, b []string) []string { + if len(b) == 0 { + return a + } + + rv := make([]string, 0, len(a)) +OUTER: + for _, as := range a { + for _, bs := range b { + if as == bs { + continue OUTER + } + } + rv = append(rv, as) + } + return rv +} + +func (is *IndexSnapshot) CopyTo(d index.Directory) error { + // get the root bolt file. + w, err := d.GetWriter(filepath.Join("store", "root.bolt")) + if err != nil || w == nil { + return fmt.Errorf("failed to create the root.bolt file, err: %v", err) + } + rootFile, ok := w.(*os.File) + if !ok { + return fmt.Errorf("invalid root.bolt file found") + } + + copyBolt, err := bolt.Open(rootFile.Name(), 0o600, nil) + if err != nil { + return err + } + defer func() { + w.Close() + if cerr := copyBolt.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + // start a write transaction + tx, err := copyBolt.Begin(true) + if err != nil { + return err + } + + _, _, err = prepareBoltSnapshot(is, tx, "", is.parent.segPlugin, nil, d) + if err != nil { + _ = tx.Rollback() + return fmt.Errorf("error backing up index snapshot: %v", err) + } + + // commit bolt data + err = tx.Commit() + if err != nil { + return fmt.Errorf("error commit tx to backup root bolt: %v", err) + } + + return copyBolt.Sync() +} + +func (is *IndexSnapshot) UpdateIOStats(val uint64) { + atomic.AddUint64(&is.parent.stats.TotBytesReadAtQueryTime, val) +} + +func (is *IndexSnapshot) GetSpatialAnalyzerPlugin(typ string) ( + index.SpatialAnalyzerPlugin, error, +) { + var rv index.SpatialAnalyzerPlugin + is.m.Lock() + rv = is.parent.spatialPlugin + is.m.Unlock() + + if rv == nil { + return nil, fmt.Errorf("no spatial plugin type: %s found", typ) + } + return rv, nil +} + +func (is *IndexSnapshot) CloseCopyReader() error { + // first unmark the segments that were marked for backup by this index snapshot + is.parent.rootLock.Lock() + for _, seg := range is.segment { + var fileName string + if perSeg, ok := seg.segment.(segment.PersistedSegment); ok { + // segment is persisted + fileName = filepath.Base(perSeg.Path()) + } else { + // segment is not persisted + // the name of the segment file that is generated if the + // the segment is persisted in the future. + fileName = zapFileName(seg.id) + } + if is.parent.copyScheduled[fileName]--; is.parent.copyScheduled[fileName] <= 0 { + delete(is.parent.copyScheduled, fileName) + } + } + is.parent.rootLock.Unlock() + // close the index snapshot normally + return is.Close() +} + +func (is *IndexSnapshot) ThesaurusTermReader(ctx context.Context, thesaurusName string, term []byte) (index.ThesaurusTermReader, error) { + rv := &IndexSnapshotThesaurusTermReader{ + name: thesaurusName, + snapshot: is, + postings: make([]segment.SynonymsList, len(is.segment)), + iterators: make([]segment.SynonymsIterator, len(is.segment)), + thesauri: make([]segment.Thesaurus, len(is.segment)), + segmentOffset: 0, + } + + for i, s := range is.segment { + if synSeg, ok := s.segment.(segment.ThesaurusSegment); ok { + thes, err := synSeg.Thesaurus(thesaurusName) + if err != nil { + return nil, err + } + rv.thesauri[i] = thes + pl, err := rv.thesauri[i].SynonymsList(term, s.deleted, rv.postings[i]) + if err != nil { + return nil, err + } + rv.postings[i] = pl + + rv.iterators[i] = pl.Iterator(rv.iterators[i]) + } + } + return rv, nil +} + +func (is *IndexSnapshot) newIndexSnapshotThesaurusKeys(name string, + makeItr func(i segment.Thesaurus) segment.ThesaurusIterator, +) (*IndexSnapshotThesaurusKeys, error) { + results := make(chan *asynchSegmentResult, len(is.segment)) + var wg sync.WaitGroup + wg.Add(len(is.segment)) + for _, s := range is.segment { + go func(s *SegmentSnapshot) { + defer wg.Done() + if synSeg, ok := s.segment.(segment.ThesaurusSegment); ok { + thes, err := synSeg.Thesaurus(name) + if err != nil { + results <- &asynchSegmentResult{err: err} + } else { + results <- &asynchSegmentResult{thesItr: makeItr(thes)} + } + } + }(s) + } + // Close the channel after all goroutines complete + go func() { + wg.Wait() + close(results) + }() + + var err error + rv := &IndexSnapshotThesaurusKeys{ + snapshot: is, + cursors: make([]*segmentThesCursor, 0, len(is.segment)), + } + for asr := range results { + if asr.err != nil && err == nil { + err = asr.err + } else { + next, err2 := asr.thesItr.Next() + if err2 != nil && err == nil { + err = err2 + } + if next != nil { + rv.cursors = append(rv.cursors, &segmentThesCursor{ + itr: asr.thesItr, + curr: *next, + }) + } + } + } + // after ensuring we've read all items on channel + if err != nil { + return nil, err + } + + return rv, nil +} + +func (is *IndexSnapshot) ThesaurusKeys(name string) (index.ThesaurusKeys, error) { + return is.newIndexSnapshotThesaurusKeys(name, func(is segment.Thesaurus) segment.ThesaurusIterator { + return is.AutomatonIterator(nil, nil, nil) + }) +} + +func (is *IndexSnapshot) ThesaurusKeysFuzzy(name string, + term string, fuzziness int, prefix string, +) (index.ThesaurusKeys, error) { + a, err := is.getLevAutomaton(term, uint8(fuzziness)) + if err != nil { + return nil, err + } + var prefixBeg, prefixEnd []byte + if prefix != "" { + prefixBeg = []byte(prefix) + prefixEnd = calculateExclusiveEndFromPrefix(prefixBeg) + } + return is.newIndexSnapshotThesaurusKeys(name, func(is segment.Thesaurus) segment.ThesaurusIterator { + return is.AutomatonIterator(a, prefixBeg, prefixEnd) + }) +} + +func (is *IndexSnapshot) ThesaurusKeysPrefix(name string, + termPrefix []byte, +) (index.ThesaurusKeys, error) { + termPrefixEnd := calculateExclusiveEndFromPrefix(termPrefix) + return is.newIndexSnapshotThesaurusKeys(name, func(is segment.Thesaurus) segment.ThesaurusIterator { + return is.AutomatonIterator(nil, termPrefix, termPrefixEnd) + }) +} + +func (is *IndexSnapshot) ThesaurusKeysRegexp(name string, + termRegex string, +) (index.ThesaurusKeys, error) { + a, prefixBeg, prefixEnd, err := parseRegexp(termRegex) + if err != nil { + return nil, err + } + return is.newIndexSnapshotThesaurusKeys(name, func(is segment.Thesaurus) segment.ThesaurusIterator { + return is.AutomatonIterator(a, prefixBeg, prefixEnd) + }) +} + +func (is *IndexSnapshot) UpdateSynonymSearchCount(delta uint64) { + atomic.AddUint64(&is.parent.stats.TotSynonymSearches, delta) +} diff --git a/index/scorch/snapshot_index_dict.go b/index/scorch/snapshot_index_dict.go new file mode 100644 index 0000000..2ae789c --- /dev/null +++ b/index/scorch/snapshot_index_dict.go @@ -0,0 +1,119 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "container/heap" + + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +type segmentDictCursor struct { + dict segment.TermDictionary + itr segment.DictionaryIterator + curr index.DictEntry +} + +type IndexSnapshotFieldDict struct { + cardinality int + bytesRead uint64 + + snapshot *IndexSnapshot + cursors []*segmentDictCursor + entry index.DictEntry +} + +func (i *IndexSnapshotFieldDict) BytesRead() uint64 { + return i.bytesRead +} + +func (i *IndexSnapshotFieldDict) Len() int { return len(i.cursors) } +func (i *IndexSnapshotFieldDict) Less(a, b int) bool { + return i.cursors[a].curr.Term < i.cursors[b].curr.Term +} +func (i *IndexSnapshotFieldDict) Swap(a, b int) { + i.cursors[a], i.cursors[b] = i.cursors[b], i.cursors[a] +} + +func (i *IndexSnapshotFieldDict) Push(x interface{}) { + i.cursors = append(i.cursors, x.(*segmentDictCursor)) +} + +func (i *IndexSnapshotFieldDict) Pop() interface{} { + n := len(i.cursors) + x := i.cursors[n-1] + i.cursors = i.cursors[0 : n-1] + return x +} + +func (i *IndexSnapshotFieldDict) Next() (*index.DictEntry, error) { + if len(i.cursors) == 0 { + return nil, nil + } + i.entry = i.cursors[0].curr + next, err := i.cursors[0].itr.Next() + if err != nil { + return nil, err + } + if next == nil { + // at end of this cursor, remove it + heap.Pop(i) + } else { + // modified heap, fix it + i.cursors[0].curr = *next + heap.Fix(i, 0) + } + // look for any other entries with the exact same term + for len(i.cursors) > 0 && i.cursors[0].curr.Term == i.entry.Term { + i.entry.Count += i.cursors[0].curr.Count + next, err := i.cursors[0].itr.Next() + if err != nil { + return nil, err + } + if next == nil { + // at end of this cursor, remove it + heap.Pop(i) + } else { + // modified heap, fix it + i.cursors[0].curr = *next + heap.Fix(i, 0) + } + } + + return &i.entry, nil +} + +func (i *IndexSnapshotFieldDict) Cardinality() int { + return i.cardinality +} + +func (i *IndexSnapshotFieldDict) Close() error { + return nil +} + +func (i *IndexSnapshotFieldDict) Contains(key []byte) (bool, error) { + if len(i.cursors) == 0 { + return false, nil + } + + for _, cursor := range i.cursors { + if found, _ := cursor.dict.Contains(key); found { + return true, nil + } + } + + return false, nil +} diff --git a/index/scorch/snapshot_index_doc.go b/index/scorch/snapshot_index_doc.go new file mode 100644 index 0000000..0a979bf --- /dev/null +++ b/index/scorch/snapshot_index_doc.go @@ -0,0 +1,80 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "bytes" + "reflect" + + "github.com/RoaringBitmap/roaring/v2" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeIndexSnapshotDocIDReader int + +func init() { + var isdr IndexSnapshotDocIDReader + reflectStaticSizeIndexSnapshotDocIDReader = int(reflect.TypeOf(isdr).Size()) +} + +type IndexSnapshotDocIDReader struct { + snapshot *IndexSnapshot + iterators []roaring.IntIterable + segmentOffset int +} + +func (i *IndexSnapshotDocIDReader) Size() int { + return reflectStaticSizeIndexSnapshotDocIDReader + size.SizeOfPtr +} + +func (i *IndexSnapshotDocIDReader) Next() (index.IndexInternalID, error) { + for i.segmentOffset < len(i.iterators) { + if !i.iterators[i.segmentOffset].HasNext() { + i.segmentOffset++ + continue + } + next := i.iterators[i.segmentOffset].Next() + // make segment number into global number by adding offset + globalOffset := i.snapshot.offsets[i.segmentOffset] + return docNumberToBytes(nil, uint64(next)+globalOffset), nil + } + return nil, nil +} + +func (i *IndexSnapshotDocIDReader) Advance(ID index.IndexInternalID) (index.IndexInternalID, error) { + // FIXME do something better + next, err := i.Next() + if err != nil { + return nil, err + } + if next == nil { + return nil, nil + } + for bytes.Compare(next, ID) < 0 { + next, err = i.Next() + if err != nil { + return nil, err + } + if next == nil { + break + } + } + return next, nil +} + +func (i *IndexSnapshotDocIDReader) Close() error { + return nil +} diff --git a/index/scorch/snapshot_index_str.go b/index/scorch/snapshot_index_str.go new file mode 100644 index 0000000..c73b56f --- /dev/null +++ b/index/scorch/snapshot_index_str.go @@ -0,0 +1,79 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "reflect" + + "github.com/blevesearch/bleve/v2/size" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +var reflectStaticSizeIndexSnapshotThesaurusTermReader int + +func init() { + var istr IndexSnapshotThesaurusTermReader + reflectStaticSizeIndexSnapshotThesaurusTermReader = int(reflect.TypeOf(istr).Size()) +} + +type IndexSnapshotThesaurusTermReader struct { + name string + snapshot *IndexSnapshot + thesauri []segment.Thesaurus + postings []segment.SynonymsList + iterators []segment.SynonymsIterator + segmentOffset int +} + +func (i *IndexSnapshotThesaurusTermReader) Size() int { + sizeInBytes := reflectStaticSizeIndexSnapshotThesaurusTermReader + size.SizeOfPtr + + len(i.name) + size.SizeOfString + + for _, postings := range i.postings { + if postings != nil { + sizeInBytes += postings.Size() + } + } + + for _, iterator := range i.iterators { + if iterator != nil { + sizeInBytes += iterator.Size() + } + } + + return sizeInBytes +} + +func (i *IndexSnapshotThesaurusTermReader) Next() (string, error) { + // find the next hit + for i.segmentOffset < len(i.iterators) { + if i.iterators[i.segmentOffset] != nil { + next, err := i.iterators[i.segmentOffset].Next() + if err != nil { + return "", err + } + if next != nil { + synTerm := next.Term() + return synTerm, nil + } + } + i.segmentOffset++ + } + return "", nil +} + +func (i *IndexSnapshotThesaurusTermReader) Close() error { + return nil +} diff --git a/index/scorch/snapshot_index_test.go b/index/scorch/snapshot_index_test.go new file mode 100644 index 0000000..bf38fba --- /dev/null +++ b/index/scorch/snapshot_index_test.go @@ -0,0 +1,90 @@ +package scorch + +import ( + "testing" + + "github.com/blevesearch/vellum" +) + +func TestIndexSnapshot_getLevAutomaton(t *testing.T) { + // Create a dummy IndexSnapshot (parent doesn't matter for this method) + is := &IndexSnapshot{} + + tests := []struct { + name string + term string + fuzziness uint8 + expectError bool + errorMsg string // Optional: check specific error message + }{ + { + name: "fuzziness 1", + term: "test", + fuzziness: 1, + expectError: false, + }, + { + name: "fuzziness 2", + term: "another", + fuzziness: 2, + expectError: false, + }, + { + name: "fuzziness 0", + term: "zero", + fuzziness: 0, + expectError: true, + errorMsg: "fuzziness exceeds the max limit", + }, + { + name: "fuzziness 3", + term: "three", + fuzziness: 3, + expectError: true, + errorMsg: "fuzziness exceeds the max limit", + }, + { + name: "empty term fuzziness 1", + term: "", + fuzziness: 1, + expectError: false, + }, + { + name: "empty term fuzziness 2", + term: "", + fuzziness: 2, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotAutomaton, err := is.getLevAutomaton(tt.term, tt.fuzziness) + + if tt.expectError { + if err == nil { + t.Errorf("getLevAutomaton() expected an error but got nil") + } else if tt.errorMsg != "" && err.Error() != tt.errorMsg { + t.Errorf("getLevAutomaton() expected error msg %q but got %q", tt.errorMsg, err.Error()) + } + if gotAutomaton != nil { + t.Errorf("getLevAutomaton() expected nil automaton on error but got %v", gotAutomaton) + } + } else { + if err != nil { + t.Errorf("getLevAutomaton() got unexpected error: %v", err) + } + if gotAutomaton == nil { + t.Errorf("getLevAutomaton() expected a valid automaton but got nil") + } + // Optional: Check type if needed, though non-nil is usually sufficient + _, ok := gotAutomaton.(vellum.Automaton) + if !ok { + t.Errorf("getLevAutomaton() returned type is not vellum.Automaton") + } + } + }) + } +} + +// Add other tests for snapshot_index.go below if needed... diff --git a/index/scorch/snapshot_index_tfr.go b/index/scorch/snapshot_index_tfr.go new file mode 100644 index 0000000..48ba356 --- /dev/null +++ b/index/scorch/snapshot_index_tfr.go @@ -0,0 +1,216 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "bytes" + "context" + "fmt" + "reflect" + "sync/atomic" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +var reflectStaticSizeIndexSnapshotTermFieldReader int + +func init() { + var istfr IndexSnapshotTermFieldReader + reflectStaticSizeIndexSnapshotTermFieldReader = int(reflect.TypeOf(istfr).Size()) +} + +type IndexSnapshotTermFieldReader struct { + term []byte + field string + snapshot *IndexSnapshot + dicts []segment.TermDictionary + postings []segment.PostingsList + iterators []segment.PostingsIterator + segmentOffset int + includeFreq bool + includeNorm bool + includeTermVectors bool + currPosting segment.Posting + currID index.IndexInternalID + recycle bool + bytesRead uint64 + ctx context.Context +} + +func (i *IndexSnapshotTermFieldReader) incrementBytesRead(val uint64) { + i.bytesRead += val +} + +func (i *IndexSnapshotTermFieldReader) Size() int { + sizeInBytes := reflectStaticSizeIndexSnapshotTermFieldReader + size.SizeOfPtr + + len(i.term) + + len(i.field) + + len(i.currID) + + for _, entry := range i.postings { + sizeInBytes += entry.Size() + } + + for _, entry := range i.iterators { + sizeInBytes += entry.Size() + } + + if i.currPosting != nil { + sizeInBytes += i.currPosting.Size() + } + + return sizeInBytes +} + +func (i *IndexSnapshotTermFieldReader) Next(preAlloced *index.TermFieldDoc) (*index.TermFieldDoc, error) { + rv := preAlloced + if rv == nil { + rv = &index.TermFieldDoc{} + } + // find the next hit + for i.segmentOffset < len(i.iterators) { + prevBytesRead := i.iterators[i.segmentOffset].BytesRead() + next, err := i.iterators[i.segmentOffset].Next() + if err != nil { + return nil, err + } + if next != nil { + // make segment number into global number by adding offset + globalOffset := i.snapshot.offsets[i.segmentOffset] + nnum := next.Number() + rv.ID = docNumberToBytes(rv.ID, nnum+globalOffset) + i.postingToTermFieldDoc(next, rv) + + i.currID = rv.ID + i.currPosting = next + // postingsIterators is maintain the bytesRead stat in a cumulative fashion. + // this is because there are chances of having a series of loadChunk calls, + // and they have to be added together before sending the bytesRead at this point + // upstream. + bytesRead := i.iterators[i.segmentOffset].BytesRead() + if bytesRead > prevBytesRead { + i.incrementBytesRead(bytesRead - prevBytesRead) + } + return rv, nil + } + i.segmentOffset++ + } + return nil, nil +} + +func (i *IndexSnapshotTermFieldReader) postingToTermFieldDoc(next segment.Posting, rv *index.TermFieldDoc) { + if i.includeFreq { + rv.Freq = next.Frequency() + } + if i.includeNorm { + rv.Norm = next.Norm() + } + if i.includeTermVectors { + locs := next.Locations() + if cap(rv.Vectors) < len(locs) { + rv.Vectors = make([]*index.TermFieldVector, len(locs)) + backing := make([]index.TermFieldVector, len(locs)) + for i := range backing { + rv.Vectors[i] = &backing[i] + } + } + rv.Vectors = rv.Vectors[:len(locs)] + for i, loc := range locs { + *rv.Vectors[i] = index.TermFieldVector{ + Start: loc.Start(), + End: loc.End(), + Pos: loc.Pos(), + ArrayPositions: loc.ArrayPositions(), + Field: loc.Field(), + } + } + } +} + +func (i *IndexSnapshotTermFieldReader) Advance(ID index.IndexInternalID, preAlloced *index.TermFieldDoc) (*index.TermFieldDoc, error) { + // FIXME do something better + // for now, if we need to seek backwards, then restart from the beginning + if i.currPosting != nil && bytes.Compare(i.currID, ID) >= 0 { + i2, err := i.snapshot.TermFieldReader(context.TODO(), i.term, i.field, + i.includeFreq, i.includeNorm, i.includeTermVectors) + if err != nil { + return nil, err + } + // close the current term field reader before replacing it with a new one + _ = i.Close() + *i = *(i2.(*IndexSnapshotTermFieldReader)) + } + num, err := docInternalToNumber(ID) + if err != nil { + return nil, fmt.Errorf("error converting to doc number % x - %v", ID, err) + } + segIndex, ldocNum := i.snapshot.segmentIndexAndLocalDocNumFromGlobal(num) + if segIndex >= len(i.snapshot.segment) { + return nil, fmt.Errorf("computed segment index %d out of bounds %d", + segIndex, len(i.snapshot.segment)) + } + // skip directly to the target segment + i.segmentOffset = segIndex + next, err := i.iterators[i.segmentOffset].Advance(ldocNum) + if err != nil { + return nil, err + } + if next == nil { + // we jumped directly to the segment that should have contained it + // but it wasn't there, so reuse Next() which should correctly + // get the next hit after it (we moved i.segmentOffset) + return i.Next(preAlloced) + } + + if preAlloced == nil { + preAlloced = &index.TermFieldDoc{} + } + preAlloced.ID = docNumberToBytes(preAlloced.ID, next.Number()+ + i.snapshot.offsets[segIndex]) + i.postingToTermFieldDoc(next, preAlloced) + i.currID = preAlloced.ID + i.currPosting = next + return preAlloced, nil +} + +func (i *IndexSnapshotTermFieldReader) Count() uint64 { + var rv uint64 + for _, posting := range i.postings { + rv += posting.Count() + } + return rv +} + +func (i *IndexSnapshotTermFieldReader) Close() error { + if i.ctx != nil { + statsCallbackFn := i.ctx.Value(search.SearchIOStatsCallbackKey) + if statsCallbackFn != nil { + // essentially before you close the TFR, you must report this + // reader's bytesRead value + statsCallbackFn.(search.SearchIOStatsCallbackFunc)(i.bytesRead) + } + + search.RecordSearchCost(i.ctx, search.AddM, i.bytesRead) + } + + if i.snapshot != nil { + atomic.AddUint64(&i.snapshot.parent.stats.TotTermSearchersFinished, uint64(1)) + i.snapshot.recycleTermFieldReader(i) + } + return nil +} diff --git a/index/scorch/snapshot_index_thes.go b/index/scorch/snapshot_index_thes.go new file mode 100644 index 0000000..6f3aae8 --- /dev/null +++ b/index/scorch/snapshot_index_thes.go @@ -0,0 +1,107 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "container/heap" + + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +type segmentThesCursor struct { + thes segment.Thesaurus + itr segment.ThesaurusIterator + curr index.ThesaurusEntry +} + +type IndexSnapshotThesaurusKeys struct { + snapshot *IndexSnapshot + cursors []*segmentThesCursor + entry index.ThesaurusEntry +} + +func (i *IndexSnapshotThesaurusKeys) Len() int { return len(i.cursors) } +func (i *IndexSnapshotThesaurusKeys) Less(a, b int) bool { + return i.cursors[a].curr.Term < i.cursors[b].curr.Term +} +func (i *IndexSnapshotThesaurusKeys) Swap(a, b int) { + i.cursors[a], i.cursors[b] = i.cursors[b], i.cursors[a] +} + +func (i *IndexSnapshotThesaurusKeys) Push(x interface{}) { + i.cursors = append(i.cursors, x.(*segmentThesCursor)) +} + +func (i *IndexSnapshotThesaurusKeys) Pop() interface{} { + n := len(i.cursors) + x := i.cursors[n-1] + i.cursors = i.cursors[0 : n-1] + return x +} + +func (i *IndexSnapshotThesaurusKeys) Next() (*index.ThesaurusEntry, error) { + if len(i.cursors) == 0 { + return nil, nil + } + i.entry = i.cursors[0].curr + next, err := i.cursors[0].itr.Next() + if err != nil { + return nil, err + } + if next == nil { + // at end of this cursor, remove it + heap.Pop(i) + } else { + // modified heap, fix it + i.cursors[0].curr = *next + heap.Fix(i, 0) + } + // look for any other entries with the exact same term + for len(i.cursors) > 0 && i.cursors[0].curr.Term == i.entry.Term { + next, err := i.cursors[0].itr.Next() + if err != nil { + return nil, err + } + if next == nil { + // at end of this cursor, remove it + heap.Pop(i) + } else { + // modified heap, fix it + i.cursors[0].curr = *next + heap.Fix(i, 0) + } + } + + return &i.entry, nil +} + +func (i *IndexSnapshotThesaurusKeys) Close() error { + return nil +} + +func (i *IndexSnapshotThesaurusKeys) Contains(key []byte) (bool, error) { + if len(i.cursors) == 0 { + return false, nil + } + + for _, cursor := range i.cursors { + if found, _ := cursor.thes.Contains(key); found { + return true, nil + } + } + + return false, nil +} diff --git a/index/scorch/snapshot_index_vr.go b/index/scorch/snapshot_index_vr.go new file mode 100644 index 0000000..7c67411 --- /dev/null +++ b/index/scorch/snapshot_index_vr.go @@ -0,0 +1,165 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package scorch + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" + segment_api "github.com/blevesearch/scorch_segment_api/v2" +) + +const VectorSearchSupportedSegmentVersion = 16 + +var reflectStaticSizeIndexSnapshotVectorReader int + +func init() { + var istfr IndexSnapshotVectorReader + reflectStaticSizeIndexSnapshotVectorReader = int(reflect.TypeOf(istfr).Size()) +} + +type IndexSnapshotVectorReader struct { + vector []float32 + field string + k int64 + snapshot *IndexSnapshot + postings []segment_api.VecPostingsList + iterators []segment_api.VecPostingsIterator + segmentOffset int + currPosting segment_api.VecPosting + currID index.IndexInternalID + ctx context.Context + + searchParams json.RawMessage + eligibleSelector index.EligibleDocumentSelector +} + +func (i *IndexSnapshotVectorReader) Size() int { + sizeInBytes := reflectStaticSizeIndexSnapshotVectorReader + size.SizeOfPtr + + len(i.vector)*size.SizeOfFloat32 + + len(i.field) + + len(i.currID) + + for _, entry := range i.postings { + sizeInBytes += entry.Size() + } + + for _, entry := range i.iterators { + sizeInBytes += entry.Size() + } + + if i.currPosting != nil { + sizeInBytes += i.currPosting.Size() + } + + return sizeInBytes +} + +func (i *IndexSnapshotVectorReader) Next(preAlloced *index.VectorDoc) ( + *index.VectorDoc, error) { + rv := preAlloced + if rv == nil { + rv = &index.VectorDoc{} + } + + for i.segmentOffset < len(i.iterators) { + next, err := i.iterators[i.segmentOffset].Next() + if err != nil { + return nil, err + } + if next != nil { + // make segment number into global number by adding offset + globalOffset := i.snapshot.offsets[i.segmentOffset] + nnum := next.Number() + rv.ID = docNumberToBytes(rv.ID, nnum+globalOffset) + rv.Score = float64(next.Score()) + + i.currID = rv.ID + i.currPosting = next + + return rv, nil + } + i.segmentOffset++ + } + + return nil, nil +} + +func (i *IndexSnapshotVectorReader) Advance(ID index.IndexInternalID, + preAlloced *index.VectorDoc) (*index.VectorDoc, error) { + + if i.currPosting != nil && bytes.Compare(i.currID, ID) >= 0 { + i2, err := i.snapshot.VectorReader(i.ctx, i.vector, i.field, i.k, + i.searchParams, i.eligibleSelector) + if err != nil { + return nil, err + } + // close the current term field reader before replacing it with a new one + _ = i.Close() + *i = *(i2.(*IndexSnapshotVectorReader)) + } + + num, err := docInternalToNumber(ID) + if err != nil { + return nil, fmt.Errorf("error converting to doc number % x - %v", ID, err) + } + segIndex, ldocNum := i.snapshot.segmentIndexAndLocalDocNumFromGlobal(num) + if segIndex >= len(i.snapshot.segment) { + return nil, fmt.Errorf("computed segment index %d out of bounds %d", + segIndex, len(i.snapshot.segment)) + } + // skip directly to the target segment + i.segmentOffset = segIndex + next, err := i.iterators[i.segmentOffset].Advance(ldocNum) + if err != nil { + return nil, err + } + if next == nil { + // we jumped directly to the segment that should have contained it + // but it wasn't there, so reuse Next() which should correctly + // get the next hit after it (we moved i.segmentOffset) + return i.Next(preAlloced) + } + + if preAlloced == nil { + preAlloced = &index.VectorDoc{} + } + preAlloced.ID = docNumberToBytes(preAlloced.ID, next.Number()+ + i.snapshot.offsets[segIndex]) + i.currID = preAlloced.ID + i.currPosting = next + return preAlloced, nil +} + +func (i *IndexSnapshotVectorReader) Count() uint64 { + var rv uint64 + for _, posting := range i.postings { + rv += posting.Count() + } + return rv +} + +func (i *IndexSnapshotVectorReader) Close() error { + // TODO Consider if any scope of recycling here. + return nil +} diff --git a/index/scorch/snapshot_segment.go b/index/scorch/snapshot_segment.go new file mode 100644 index 0000000..ec65bf8 --- /dev/null +++ b/index/scorch/snapshot_segment.go @@ -0,0 +1,340 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "bytes" + "os" + "sync" + "sync/atomic" + + "github.com/RoaringBitmap/roaring/v2" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +var TermSeparator byte = 0xff + +var TermSeparatorSplitSlice = []byte{TermSeparator} + +type SegmentSnapshot struct { + // this flag is needed to identify whether this + // segment was mmaped recently, in which case + // we consider the loading cost of the metadata + // as part of IO stats. + mmaped uint32 + id uint64 + segment segment.Segment + deleted *roaring.Bitmap + creator string + stats *fieldStats + + cachedMeta *cachedMeta + + cachedDocs *cachedDocs +} + +func (s *SegmentSnapshot) Segment() segment.Segment { + return s.segment +} + +func (s *SegmentSnapshot) Deleted() *roaring.Bitmap { + return s.deleted +} + +func (s *SegmentSnapshot) Id() uint64 { + return s.id +} + +func (s *SegmentSnapshot) FullSize() int64 { + return int64(s.segment.Count()) +} + +func (s *SegmentSnapshot) LiveSize() int64 { + return int64(s.Count()) +} + +func (s *SegmentSnapshot) HasVector() bool { + // number of vectors, for each vector field in the segment + numVecs := s.stats.Fetch()["num_vectors"] + return len(numVecs) > 0 +} + +func (s *SegmentSnapshot) FileSize() int64 { + ps, ok := s.segment.(segment.PersistedSegment) + if !ok { + return 0 + } + + path := ps.Path() + if path == "" { + return 0 + } + + fi, err := os.Stat(path) + if err != nil { + return 0 + } + + return fi.Size() +} + +func (s *SegmentSnapshot) Close() error { + return s.segment.Close() +} + +func (s *SegmentSnapshot) VisitDocument(num uint64, visitor segment.StoredFieldValueVisitor) error { + return s.segment.VisitStoredFields(num, visitor) +} + +func (s *SegmentSnapshot) DocID(num uint64) ([]byte, error) { + return s.segment.DocID(num) +} + +func (s *SegmentSnapshot) Count() uint64 { + rv := s.segment.Count() + if s.deleted != nil { + rv -= s.deleted.GetCardinality() + } + return rv +} + +func (s *SegmentSnapshot) DocNumbers(docIDs []string) (*roaring.Bitmap, error) { + rv, err := s.segment.DocNumbers(docIDs) + if err != nil { + return nil, err + } + if s.deleted != nil { + rv.AndNot(s.deleted) + } + return rv, nil +} + +// DocNumbersLive returns a bitmap containing doc numbers for all live docs +func (s *SegmentSnapshot) DocNumbersLive() *roaring.Bitmap { + rv := roaring.NewBitmap() + rv.AddRange(0, s.segment.Count()) + if s.deleted != nil { + rv.AndNot(s.deleted) + } + return rv +} + +func (s *SegmentSnapshot) Fields() []string { + return s.segment.Fields() +} + +func (s *SegmentSnapshot) Size() (rv int) { + rv = s.segment.Size() + if s.deleted != nil { + rv += int(s.deleted.GetSizeInBytes()) + } + rv += s.cachedDocs.Size() + return +} + +type cachedFieldDocs struct { + m sync.Mutex + readyCh chan struct{} // closed when the cachedFieldDocs.docs is ready to be used. + err error // Non-nil if there was an error when preparing this cachedFieldDocs. + docs map[uint64][]byte // Keyed by localDocNum, value is a list of terms delimited by 0xFF. + size uint64 +} + +func (cfd *cachedFieldDocs) Size() int { + var rv int + cfd.m.Lock() + for _, entry := range cfd.docs { + rv += 8 /* size of uint64 */ + len(entry) + } + cfd.m.Unlock() + return rv +} + +func (cfd *cachedFieldDocs) prepareField(field string, ss *SegmentSnapshot) { + cfd.m.Lock() + defer func() { + close(cfd.readyCh) + cfd.m.Unlock() + }() + + cfd.size += uint64(size.SizeOfUint64) /* size field */ + dict, err := ss.segment.Dictionary(field) + if err != nil { + cfd.err = err + return + } + + var postings segment.PostingsList + var postingsItr segment.PostingsIterator + + dictItr := dict.AutomatonIterator(nil, nil, nil) + next, err := dictItr.Next() + for err == nil && next != nil { + var err1 error + postings, err1 = dict.PostingsList([]byte(next.Term), nil, postings) + if err1 != nil { + cfd.err = err1 + return + } + + cfd.size += uint64(size.SizeOfUint64) /* map key */ + postingsItr = postings.Iterator(false, false, false, postingsItr) + nextPosting, err2 := postingsItr.Next() + for err2 == nil && nextPosting != nil { + docNum := nextPosting.Number() + cfd.docs[docNum] = append(cfd.docs[docNum], []byte(next.Term)...) + cfd.docs[docNum] = append(cfd.docs[docNum], TermSeparator) + cfd.size += uint64(len(next.Term) + 1) // map value + nextPosting, err2 = postingsItr.Next() + } + + if err2 != nil { + cfd.err = err2 + return + } + + next, err = dictItr.Next() + } + + if err != nil { + cfd.err = err + return + } +} + +type cachedDocs struct { + size uint64 + m sync.Mutex // As the cache is asynchronously prepared, need a lock + cache map[string]*cachedFieldDocs // Keyed by field +} + +func (c *cachedDocs) prepareFields(wantedFields []string, ss *SegmentSnapshot) error { + c.m.Lock() + + if c.cache == nil { + c.cache = make(map[string]*cachedFieldDocs, len(ss.Fields())) + } + + for _, field := range wantedFields { + _, exists := c.cache[field] + if !exists { + c.cache[field] = &cachedFieldDocs{ + readyCh: make(chan struct{}), + docs: make(map[uint64][]byte), + } + + go c.cache[field].prepareField(field, ss) + } + } + + for _, field := range wantedFields { + cachedFieldDocs := c.cache[field] + c.m.Unlock() + <-cachedFieldDocs.readyCh + + if cachedFieldDocs.err != nil { + return cachedFieldDocs.err + } + c.m.Lock() + } + + c.updateSizeLOCKED() + + c.m.Unlock() + return nil +} + +// hasFields returns true if the cache has all the given fields +func (c *cachedDocs) hasFields(fields []string) bool { + c.m.Lock() + for _, field := range fields { + if _, exists := c.cache[field]; !exists { + c.m.Unlock() + return false // found a field not in cache + } + } + c.m.Unlock() + return true +} + +func (c *cachedDocs) Size() int { + return int(atomic.LoadUint64(&c.size)) +} + +func (c *cachedDocs) updateSizeLOCKED() { + sizeInBytes := 0 + for k, v := range c.cache { // cachedFieldDocs + sizeInBytes += len(k) + if v != nil { + sizeInBytes += v.Size() + } + } + atomic.StoreUint64(&c.size, uint64(sizeInBytes)) +} + +func (c *cachedDocs) visitDoc(localDocNum uint64, + fields []string, visitor index.DocValueVisitor) { + c.m.Lock() + + for _, field := range fields { + if cachedFieldDocs, exists := c.cache[field]; exists { + c.m.Unlock() + <-cachedFieldDocs.readyCh + c.m.Lock() + + if tlist, exists := cachedFieldDocs.docs[localDocNum]; exists { + for { + i := bytes.Index(tlist, TermSeparatorSplitSlice) + if i < 0 { + break + } + visitor(field, tlist[0:i]) + tlist = tlist[i+1:] + } + } + } + } + + c.m.Unlock() +} + +// the purpose of the cachedMeta is to simply allow the user of this type to record +// and cache certain meta data information (specific to the segment) that can be +// used across calls to save compute on the same. +// for example searcher creations on the same index snapshot can use this struct +// to help and fetch the backing index size information which can be used in +// memory usage calculation thereby deciding whether to allow a query or not. +type cachedMeta struct { + m sync.RWMutex + meta map[string]interface{} +} + +func (c *cachedMeta) updateMeta(field string, val interface{}) { + c.m.Lock() + if c.meta == nil { + c.meta = make(map[string]interface{}) + } + c.meta[field] = val + c.m.Unlock() +} + +func (c *cachedMeta) fetchMeta(field string) (rv interface{}) { + c.m.RLock() + rv = c.meta[field] + c.m.RUnlock() + return rv +} diff --git a/index/scorch/snapshot_vector_index.go b/index/scorch/snapshot_vector_index.go new file mode 100644 index 0000000..db5e067 --- /dev/null +++ b/index/scorch/snapshot_vector_index.go @@ -0,0 +1,85 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package scorch + +import ( + "context" + "encoding/json" + "fmt" + + index "github.com/blevesearch/bleve_index_api" + segment_api "github.com/blevesearch/scorch_segment_api/v2" +) + +func (is *IndexSnapshot) VectorReader(ctx context.Context, vector []float32, + field string, k int64, searchParams json.RawMessage, + eligibleSelector index.EligibleDocumentSelector) ( + index.VectorReader, error) { + rv := &IndexSnapshotVectorReader{ + vector: vector, + field: field, + k: k, + snapshot: is, + searchParams: searchParams, + eligibleSelector: eligibleSelector, + } + + if rv.postings == nil { + rv.postings = make([]segment_api.VecPostingsList, len(is.segment)) + } + if rv.iterators == nil { + rv.iterators = make([]segment_api.VecPostingsIterator, len(is.segment)) + } + // initialize postings and iterators within the OptimizeVR's Finish() + return rv, nil +} + +// eligibleDocumentSelector is used to filter out documents that are eligible for +// the KNN search from a pre-filter query. +type eligibleDocumentSelector struct { + // segment ID -> segment local doc nums + eligibleDocNums map[int][]uint64 + is *IndexSnapshot +} + +// SegmentEligibleDocs returns the list of eligible local doc numbers for the given segment. +func (eds *eligibleDocumentSelector) SegmentEligibleDocs(segmentID int) []uint64 { + return eds.eligibleDocNums[segmentID] +} + +// AddEligibleDocumentMatch adds a document match to the list of eligible documents. +func (eds *eligibleDocumentSelector) AddEligibleDocumentMatch(id index.IndexInternalID) error { + if eds.is == nil { + return fmt.Errorf("eligibleDocumentSelector is not initialized with IndexSnapshot") + } + // Get the segment number and the local doc number for this document. + segIdx, docNum, err := eds.is.segmentIndexAndLocalDocNum(id) + if err != nil { + return err + } + // Add the local doc number to the list of eligible doc numbers for this segment. + eds.eligibleDocNums[segIdx] = append(eds.eligibleDocNums[segIdx], docNum) + return nil +} + +func (is *IndexSnapshot) NewEligibleDocumentSelector() index.EligibleDocumentSelector { + return &eligibleDocumentSelector{ + eligibleDocNums: map[int][]uint64{}, + is: is, + } +} diff --git a/index/scorch/stats.go b/index/scorch/stats.go new file mode 100644 index 0000000..9abc8ba --- /dev/null +++ b/index/scorch/stats.go @@ -0,0 +1,160 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "reflect" + "sync/atomic" + + "github.com/blevesearch/bleve/v2/util" +) + +// Stats tracks statistics about the index, fields that are +// prefixed like CurXxxx are gauges (can go up and down), +// and fields that are prefixed like TotXxxx are monotonically +// increasing counters. +type Stats struct { + TotUpdates uint64 + TotDeletes uint64 + + TotBatches uint64 + TotBatchesEmpty uint64 + TotBatchIntroTime uint64 + MaxBatchIntroTime uint64 + + CurRootEpoch uint64 + LastPersistedEpoch uint64 + LastMergedEpoch uint64 + + TotOnErrors uint64 + + TotAnalysisTime uint64 + TotIndexTime uint64 + + TotIndexedPlainTextBytes uint64 + + TotBytesReadAtQueryTime uint64 + TotBytesWrittenAtIndexTime uint64 + + TotTermSearchersStarted uint64 + TotTermSearchersFinished uint64 + + TotKNNSearches uint64 + TotSynonymSearches uint64 + + TotEventTriggerStarted uint64 + TotEventTriggerCompleted uint64 + + TotIntroduceLoop uint64 + TotIntroduceSegmentBeg uint64 + TotIntroduceSegmentEnd uint64 + TotIntroducePersistBeg uint64 + TotIntroducePersistEnd uint64 + TotIntroduceMergeBeg uint64 + TotIntroduceMergeEnd uint64 + TotIntroduceRevertBeg uint64 + TotIntroduceRevertEnd uint64 + + TotIntroducedItems uint64 + TotIntroducedSegmentsBatch uint64 + TotIntroducedSegmentsMerge uint64 + + TotPersistLoopBeg uint64 + TotPersistLoopErr uint64 + TotPersistLoopProgress uint64 + TotPersistLoopWait uint64 + TotPersistLoopWaitNotified uint64 + TotPersistLoopEnd uint64 + + TotPersistedItems uint64 + TotItemsToPersist uint64 + TotPersistedSegments uint64 + TotMutationsFiltered uint64 + + TotPersisterSlowMergerPause uint64 + TotPersisterSlowMergerResume uint64 + + TotPersisterNapPauseCompleted uint64 + TotPersisterMergerNapBreak uint64 + + TotFileMergeLoopBeg uint64 + TotFileMergeLoopErr uint64 + TotFileMergeLoopEnd uint64 + + TotFileMergeForceOpsStarted uint64 + TotFileMergeForceOpsCompleted uint64 + + TotFileMergePlan uint64 + TotFileMergePlanErr uint64 + TotFileMergePlanNone uint64 + TotFileMergePlanOk uint64 + + TotFileMergePlanTasks uint64 + TotFileMergePlanTasksDone uint64 + TotFileMergePlanTasksErr uint64 + TotFileMergePlanTasksSegments uint64 + TotFileMergePlanTasksSegmentsEmpty uint64 + + TotFileMergeSegmentsEmpty uint64 + TotFileMergeSegments uint64 + TotFileSegmentsAtRoot uint64 + TotFileMergeWrittenBytes uint64 + + TotFileMergeZapBeg uint64 + TotFileMergeZapEnd uint64 + TotFileMergeZapTime uint64 + MaxFileMergeZapTime uint64 + TotFileMergeZapIntroductionTime uint64 + MaxFileMergeZapIntroductionTime uint64 + + TotFileMergeIntroductions uint64 + TotFileMergeIntroductionsDone uint64 + TotFileMergeIntroductionsSkipped uint64 + TotFileMergeIntroductionsObsoleted uint64 + + CurFilesIneligibleForRemoval uint64 + TotSnapshotsRemovedFromMetaStore uint64 + + TotMemMergeBeg uint64 + TotMemMergeErr uint64 + TotMemMergeDone uint64 + TotMemMergeZapBeg uint64 + TotMemMergeZapEnd uint64 + TotMemMergeZapTime uint64 + MaxMemMergeZapTime uint64 + TotMemMergeSegments uint64 + TotMemorySegmentsAtRoot uint64 +} + +// atomically populates the returned map +func (s *Stats) ToMap() map[string]interface{} { + m := map[string]interface{}{} + sve := reflect.ValueOf(s).Elem() + svet := sve.Type() + for i := 0; i < svet.NumField(); i++ { + svef := sve.Field(i) + if svef.CanAddr() { + svefp := svef.Addr().Interface() + m[svet.Field(i).Name] = atomic.LoadUint64(svefp.(*uint64)) + } + } + return m +} + +// MarshalJSON implements json.Marshaler, and in contrast to standard +// json marshaling provides atomic safety +func (s *Stats) MarshalJSON() ([]byte, error) { + return util.MarshalJSON(s.ToMap()) +} diff --git a/index/scorch/unadorned.go b/index/scorch/unadorned.go new file mode 100644 index 0000000..411ef2a --- /dev/null +++ b/index/scorch/unadorned.go @@ -0,0 +1,182 @@ +// Copyright (c) 2020 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorch + +import ( + "math" + "reflect" + + "github.com/RoaringBitmap/roaring/v2" + segment "github.com/blevesearch/scorch_segment_api/v2" +) + +var reflectStaticSizeUnadornedPostingsIteratorBitmap int +var reflectStaticSizeUnadornedPostingsIterator1Hit int +var reflectStaticSizeUnadornedPosting int + +func init() { + var pib unadornedPostingsIteratorBitmap + reflectStaticSizeUnadornedPostingsIteratorBitmap = int(reflect.TypeOf(pib).Size()) + var pi1h unadornedPostingsIterator1Hit + reflectStaticSizeUnadornedPostingsIterator1Hit = int(reflect.TypeOf(pi1h).Size()) + var up UnadornedPosting + reflectStaticSizeUnadornedPosting = int(reflect.TypeOf(up).Size()) +} + +type unadornedPostingsIteratorBitmap struct { + actual roaring.IntPeekable + actualBM *roaring.Bitmap +} + +func (i *unadornedPostingsIteratorBitmap) Next() (segment.Posting, error) { + return i.nextAtOrAfter(0) +} + +func (i *unadornedPostingsIteratorBitmap) Advance(docNum uint64) (segment.Posting, error) { + return i.nextAtOrAfter(docNum) +} + +func (i *unadornedPostingsIteratorBitmap) nextAtOrAfter(atOrAfter uint64) (segment.Posting, error) { + docNum, exists := i.nextDocNumAtOrAfter(atOrAfter) + if !exists { + return nil, nil + } + return UnadornedPosting(docNum), nil +} + +func (i *unadornedPostingsIteratorBitmap) nextDocNumAtOrAfter(atOrAfter uint64) (uint64, bool) { + if i.actual == nil || !i.actual.HasNext() { + return 0, false + } + i.actual.AdvanceIfNeeded(uint32(atOrAfter)) + + if !i.actual.HasNext() { + return 0, false // couldn't find anything + } + + return uint64(i.actual.Next()), true +} + +func (i *unadornedPostingsIteratorBitmap) Size() int { + return reflectStaticSizeUnadornedPostingsIteratorBitmap +} + +func (i *unadornedPostingsIteratorBitmap) BytesRead() uint64 { + return 0 +} + +func (i *unadornedPostingsIteratorBitmap) BytesWritten() uint64 { + return 0 +} + +func (i *unadornedPostingsIteratorBitmap) ResetBytesRead(uint64) {} + +func (i *unadornedPostingsIteratorBitmap) ActualBitmap() *roaring.Bitmap { + return i.actualBM +} + +func (i *unadornedPostingsIteratorBitmap) DocNum1Hit() (uint64, bool) { + return 0, false +} + +func (i *unadornedPostingsIteratorBitmap) ReplaceActual(actual *roaring.Bitmap) { + i.actualBM = actual + i.actual = actual.Iterator() +} + +func newUnadornedPostingsIteratorFromBitmap(bm *roaring.Bitmap) segment.PostingsIterator { + return &unadornedPostingsIteratorBitmap{ + actualBM: bm, + actual: bm.Iterator(), + } +} + +const docNum1HitFinished = math.MaxUint64 + +type unadornedPostingsIterator1Hit struct { + docNum uint64 +} + +func (i *unadornedPostingsIterator1Hit) Next() (segment.Posting, error) { + return i.nextAtOrAfter(0) +} + +func (i *unadornedPostingsIterator1Hit) Advance(docNum uint64) (segment.Posting, error) { + return i.nextAtOrAfter(docNum) +} + +func (i *unadornedPostingsIterator1Hit) nextAtOrAfter(atOrAfter uint64) (segment.Posting, error) { + docNum, exists := i.nextDocNumAtOrAfter(atOrAfter) + if !exists { + return nil, nil + } + return UnadornedPosting(docNum), nil +} + +func (i *unadornedPostingsIterator1Hit) nextDocNumAtOrAfter(atOrAfter uint64) (uint64, bool) { + if i.docNum == docNum1HitFinished { + return 0, false + } + if i.docNum < atOrAfter { + // advanced past our 1-hit + i.docNum = docNum1HitFinished // consume our 1-hit docNum + return 0, false + } + docNum := i.docNum + i.docNum = docNum1HitFinished // consume our 1-hit docNum + return docNum, true +} + +func (i *unadornedPostingsIterator1Hit) Size() int { + return reflectStaticSizeUnadornedPostingsIterator1Hit +} + +func (i *unadornedPostingsIterator1Hit) BytesRead() uint64 { + return 0 +} + +func (i *unadornedPostingsIterator1Hit) BytesWritten() uint64 { + return 0 +} + +func (i *unadornedPostingsIterator1Hit) ResetBytesRead(uint64) {} + +func newUnadornedPostingsIteratorFrom1Hit(docNum1Hit uint64) segment.PostingsIterator { + return &unadornedPostingsIterator1Hit{ + docNum1Hit, + } +} + +type UnadornedPosting uint64 + +func (p UnadornedPosting) Number() uint64 { + return uint64(p) +} + +func (p UnadornedPosting) Frequency() uint64 { + return 0 +} + +func (p UnadornedPosting) Norm() float64 { + return 0 +} + +func (p UnadornedPosting) Locations() []segment.Location { + return nil +} + +func (p UnadornedPosting) Size() int { + return reflectStaticSizeUnadornedPosting +} diff --git a/index/upsidedown/analysis.go b/index/upsidedown/analysis.go new file mode 100644 index 0000000..1ebd191 --- /dev/null +++ b/index/upsidedown/analysis.go @@ -0,0 +1,129 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + index "github.com/blevesearch/bleve_index_api" +) + +type IndexRow interface { + KeySize() int + KeyTo([]byte) (int, error) + Key() []byte + + ValueSize() int + ValueTo([]byte) (int, error) + Value() []byte +} + +type AnalysisResult struct { + DocID string + Rows []IndexRow +} + +func (udc *UpsideDownCouch) Analyze(d index.Document) *AnalysisResult { + return udc.analyze(d) +} + +func (udc *UpsideDownCouch) analyze(d index.Document) *AnalysisResult { + rv := &AnalysisResult{ + DocID: d.ID(), + Rows: make([]IndexRow, 0, 100), + } + + docIDBytes := []byte(d.ID()) + + // track our back index entries + backIndexStoredEntries := make([]*BackIndexStoreEntry, 0) + + // information we collate as we merge fields with same name + fieldTermFreqs := make(map[uint16]index.TokenFrequencies) + fieldLengths := make(map[uint16]int) + fieldIncludeTermVectors := make(map[uint16]bool) + fieldNames := make(map[uint16]string) + + analyzeField := func(field index.Field, storable bool) { + fieldIndex, newFieldRow := udc.fieldIndexOrNewRow(field.Name()) + if newFieldRow != nil { + rv.Rows = append(rv.Rows, newFieldRow) + } + fieldNames[fieldIndex] = field.Name() + + if field.Options().IsIndexed() { + field.Analyze() + fieldLength := field.AnalyzedLength() + tokenFreqs := field.AnalyzedTokenFrequencies() + existingFreqs := fieldTermFreqs[fieldIndex] + if existingFreqs == nil { + fieldTermFreqs[fieldIndex] = tokenFreqs + } else { + existingFreqs.MergeAll(field.Name(), tokenFreqs) + fieldTermFreqs[fieldIndex] = existingFreqs + } + fieldLengths[fieldIndex] += fieldLength + fieldIncludeTermVectors[fieldIndex] = field.Options().IncludeTermVectors() + } + + if storable && field.Options().IsStored() { + rv.Rows, backIndexStoredEntries = udc.storeField(docIDBytes, field, fieldIndex, rv.Rows, backIndexStoredEntries) + } + } + + // walk all the fields, record stored fields now + // place information about indexed fields into map + // this collates information across fields with + // same names (arrays) + d.VisitFields(func(field index.Field) { + analyzeField(field, true) + }) + + if d.HasComposite() { + for fieldIndex, tokenFreqs := range fieldTermFreqs { + // see if any of the composite fields need this + d.VisitComposite(func(field index.CompositeField) { + field.Compose(fieldNames[fieldIndex], fieldLengths[fieldIndex], tokenFreqs) + }) + } + + d.VisitComposite(func(field index.CompositeField) { + analyzeField(field, false) + }) + } + + rowsCapNeeded := len(rv.Rows) + 1 + for _, tokenFreqs := range fieldTermFreqs { + rowsCapNeeded += len(tokenFreqs) + } + + rv.Rows = append(make([]IndexRow, 0, rowsCapNeeded), rv.Rows...) + + backIndexTermsEntries := make([]*BackIndexTermsEntry, 0, len(fieldTermFreqs)) + + // walk through the collated information and process + // once for each indexed field (unique name) + for fieldIndex, tokenFreqs := range fieldTermFreqs { + fieldLength := fieldLengths[fieldIndex] + includeTermVectors := fieldIncludeTermVectors[fieldIndex] + + // encode this field + rv.Rows, backIndexTermsEntries = udc.indexField(docIDBytes, includeTermVectors, fieldIndex, fieldLength, tokenFreqs, rv.Rows, backIndexTermsEntries) + } + + // build the back index row + backIndexRow := NewBackIndexRow(docIDBytes, backIndexTermsEntries, backIndexStoredEntries) + rv.Rows = append(rv.Rows, backIndexRow) + + return rv +} diff --git a/index/upsidedown/analysis_test.go b/index/upsidedown/analysis_test.go new file mode 100644 index 0000000..4255346 --- /dev/null +++ b/index/upsidedown/analysis_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/null" + "github.com/blevesearch/bleve/v2/registry" + index "github.com/blevesearch/bleve_index_api" +) + +func TestAnalysisBug328(t *testing.T) { + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(standard.Name) + if err != nil { + t.Fatal(err) + } + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(null.Name, nil, analysisQueue) + if err != nil { + t.Fatal(err) + } + + d := document.NewDocument("1") + f := document.NewTextFieldCustom("title", nil, []byte("bleve"), index.IndexField|index.IncludeTermVectors, analyzer) + d.AddField(f) + f = document.NewTextFieldCustom("body", nil, []byte("bleve"), index.IndexField|index.IncludeTermVectors, analyzer) + d.AddField(f) + cf := document.NewCompositeFieldWithIndexingOptions("_all", true, []string{}, []string{}, index.IndexField|index.IncludeTermVectors) + d.AddField(cf) + + rv := idx.(*UpsideDownCouch).analyze(d) + fieldIndexes := make(map[uint16]string) + for _, row := range rv.Rows { + if row, ok := row.(*FieldRow); ok { + fieldIndexes[row.index] = row.name + } + if row, ok := row.(*TermFrequencyRow); ok && string(row.term) == "bleve" { + for _, vec := range row.vectors { + if vec.field != row.field { + if fieldIndexes[row.field] != "_all" { + t.Errorf("row named %s field %d - vector field %d", fieldIndexes[row.field], row.field, vec.field) + } + } + } + } + } +} + +func BenchmarkAnalyze(b *testing.B) { + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(standard.Name) + if err != nil { + b.Fatal(err) + } + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(null.Name, nil, analysisQueue) + if err != nil { + b.Fatal(err) + } + + d := document.NewDocument("1") + f := document.NewTextFieldWithAnalyzer("desc", nil, bleveWikiArticle1K, analyzer) + d.AddField(f) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + rv := idx.(*UpsideDownCouch).analyze(d) + if len(rv.Rows) < 92 || len(rv.Rows) > 93 { + b.Fatalf("expected 512-13 rows, got %d", len(rv.Rows)) + } + } +} + +var bleveWikiArticle1K = []byte(`Boiling liquid expanding vapor explosion +From Wikipedia, the free encyclopedia +See also: Boiler explosion and Steam explosion + +Flames subsequent to a flammable liquid BLEVE from a tanker. BLEVEs do not necessarily involve fire. + +This article's tone or style may not reflect the encyclopedic tone used on Wikipedia. See Wikipedia's guide to writing better articles for suggestions. (July 2013) +A boiling liquid expanding vapor explosion (BLEVE, /ˈblɛviː/ blev-ee) is an explosion caused by the rupture of a vessel containing a pressurized liquid above its boiling point.[1] +Contents [hide] +1 Mechanism +1.1 Water example +1.2 BLEVEs without chemical reactions +2 Fires +3 Incidents +4 Safety measures +5 See also +6 References +7 External links +Mechanism[edit] + +This section needs additional citations for verification. Please help improve this article by adding citations to reliable sources. Unsourced material may be challenged and removed. (July 2013) +There are three characteristics of liquids which are relevant to the discussion of a BLEVE:`) diff --git a/index/upsidedown/benchmark_all.sh b/index/upsidedown/benchmark_all.sh new file mode 100755 index 0000000..079fef1 --- /dev/null +++ b/index/upsidedown/benchmark_all.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +BENCHMARKS=`grep "func Benchmark" *_test.go | sed 's/.*func //' | sed s/\(.*{//` + +for BENCHMARK in $BENCHMARKS +do + go test -v -run=xxx -bench=^$BENCHMARK$ -benchtime=10s -tags 'forestdb leveldb' | grep -v ok | grep -v PASS +done diff --git a/index/upsidedown/benchmark_boltdb_test.go b/index/upsidedown/benchmark_boltdb_test.go new file mode 100644 index 0000000..5fabfa6 --- /dev/null +++ b/index/upsidedown/benchmark_boltdb_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" +) + +var boltTestConfig = map[string]interface{}{ + "path": "test", +} + +func BenchmarkBoltDBIndexing1Workers(b *testing.B) { + CommonBenchmarkIndex(b, boltdb.Name, boltTestConfig, DestroyTest, 1) +} + +func BenchmarkBoltDBIndexing2Workers(b *testing.B) { + CommonBenchmarkIndex(b, boltdb.Name, boltTestConfig, DestroyTest, 2) +} + +func BenchmarkBoltDBIndexing4Workers(b *testing.B) { + CommonBenchmarkIndex(b, boltdb.Name, boltTestConfig, DestroyTest, 4) +} + +// batches + +func BenchmarkBoltDBIndexing1Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 1, 10) +} + +func BenchmarkBoltDBIndexing2Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 2, 10) +} + +func BenchmarkBoltDBIndexing4Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 4, 10) +} + +func BenchmarkBoltDBIndexing1Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 1, 100) +} + +func BenchmarkBoltDBIndexing2Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 2, 100) +} + +func BenchmarkBoltDBIndexing4Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 4, 100) +} + +func BenchmarkBoltBIndexing1Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 1, 1000) +} + +func BenchmarkBoltBIndexing2Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 2, 1000) +} + +func BenchmarkBoltBIndexing4Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, boltdb.Name, boltTestConfig, DestroyTest, 4, 1000) +} diff --git a/index/upsidedown/benchmark_common_test.go b/index/upsidedown/benchmark_common_test.go new file mode 100644 index 0000000..758b2a2 --- /dev/null +++ b/index/upsidedown/benchmark_common_test.go @@ -0,0 +1,149 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "os" + "strconv" + "testing" + + _ "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/registry" + index "github.com/blevesearch/bleve_index_api" +) + +var benchmarkDocBodies = []string{ + "A boiling liquid expanding vapor explosion (BLEVE, /ˈblɛviː/ blev-ee) is an explosion caused by the rupture of a vessel containing a pressurized liquid above its boiling point.", + "A boiler explosion is a catastrophic failure of a boiler. As seen today, boiler explosions are of two kinds. One kind is a failure of the pressure parts of the steam and water sides. There can be many different causes, such as failure of the safety valve, corrosion of critical parts of the boiler, or low water level. Corrosion along the edges of lap joints was a common cause of early boiler explosions.", + "A boiler is a closed vessel in which water or other fluid is heated. The fluid does not necessarily boil. (In North America the term \"furnace\" is normally used if the purpose is not actually to boil the fluid.) The heated or vaporized fluid exits the boiler for use in various processes or heating applications,[1][2] including central heating, boiler-based power generation, cooking, and sanitation.", + "A pressure vessel is a closed container designed to hold gases or liquids at a pressure substantially different from the ambient pressure.", + "Pressure (symbol: p or P) is the ratio of force to the area over which that force is distributed.", + "Liquid is one of the four fundamental states of matter (the others being solid, gas, and plasma), and is the only state with a definite volume but no fixed shape.", + "The boiling point of a substance is the temperature at which the vapor pressure of the liquid equals the pressure surrounding the liquid[1][2] and the liquid changes into a vapor.", + "Vapor pressure or equilibrium vapor pressure is defined as the pressure exerted by a vapor in thermodynamic equilibrium with its condensed phases (solid or liquid) at a given temperature in a closed system.", + "Industrial gases are a group of gases that are specifically manufactured for use in a wide range of industries, which include oil and gas, petrochemicals, chemicals, power, mining, steelmaking, metals, environmental protection, medicine, pharmaceuticals, biotechnology, food, water, fertilizers, nuclear power, electronics and aerospace.", + "The expansion ratio of a liquefied and cryogenic substance is the volume of a given amount of that substance in liquid form compared to the volume of the same amount of substance in gaseous form, at room temperature and normal atmospheric pressure.", +} + +type KVStoreDestroy func() error + +func DestroyTest() error { + return os.RemoveAll("test") +} + +func CommonBenchmarkIndex(b *testing.B, storeName string, storeConfig map[string]interface{}, destroy KVStoreDestroy, analysisWorkers int) { + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed("standard") + if err != nil { + b.Fatal(err) + } + + indexDocument := document.NewDocument(""). + AddField(document.NewTextFieldWithAnalyzer("body", []uint64{}, []byte(benchmarkDocBodies[0]), analyzer)) + + b.ResetTimer() + b.StopTimer() + for i := 0; i < b.N; i++ { + analysisQueue := index.NewAnalysisQueue(analysisWorkers) + idx, err := NewUpsideDownCouch(storeName, storeConfig, analysisQueue) + if err != nil { + b.Fatal(err) + } + + err = idx.Open() + if err != nil { + b.Fatal(err) + } + indexDocument.SetID(strconv.Itoa(i)) + // just time the indexing portion + b.StartTimer() + err = idx.Update(indexDocument) + if err != nil { + b.Fatal(err) + } + b.StopTimer() + err = idx.Close() + if err != nil { + b.Fatal(err) + } + err = destroy() + if err != nil { + b.Fatal(err) + } + analysisQueue.Close() + } +} + +func CommonBenchmarkIndexBatch(b *testing.B, storeName string, storeConfig map[string]interface{}, destroy KVStoreDestroy, analysisWorkers, batchSize int) { + + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed("standard") + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + b.StopTimer() + for i := 0; i < b.N; i++ { + + analysisQueue := index.NewAnalysisQueue(analysisWorkers) + idx, err := NewUpsideDownCouch(storeName, storeConfig, analysisQueue) + if err != nil { + b.Fatal(err) + } + + err = idx.Open() + if err != nil { + b.Fatal(err) + } + + b.StartTimer() + batch := index.NewBatch() + for j := 0; j < 1000; j++ { + if j%batchSize == 0 { + if len(batch.IndexOps) > 0 { + err := idx.Batch(batch) + if err != nil { + b.Fatal(err) + } + } + batch = index.NewBatch() + } + indexDocument := document.NewDocument(""). + AddField(document.NewTextFieldWithAnalyzer("body", []uint64{}, []byte(benchmarkDocBodies[j%10]), analyzer)) + indexDocument.SetID(strconv.Itoa(i) + "-" + strconv.Itoa(j)) + batch.Update(indexDocument) + } + // close last batch + if len(batch.IndexOps) > 0 { + err := idx.Batch(batch) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() + err = idx.Close() + if err != nil { + b.Fatal(err) + } + err = destroy() + if err != nil { + b.Fatal(err) + } + analysisQueue.Close() + } +} diff --git a/index/upsidedown/benchmark_gtreap_test.go b/index/upsidedown/benchmark_gtreap_test.go new file mode 100644 index 0000000..fb508fd --- /dev/null +++ b/index/upsidedown/benchmark_gtreap_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" +) + +func BenchmarkGTreapIndexing1Workers(b *testing.B) { + CommonBenchmarkIndex(b, gtreap.Name, nil, DestroyTest, 1) +} + +func BenchmarkGTreapIndexing2Workers(b *testing.B) { + CommonBenchmarkIndex(b, gtreap.Name, nil, DestroyTest, 2) +} + +func BenchmarkGTreapIndexing4Workers(b *testing.B) { + CommonBenchmarkIndex(b, gtreap.Name, nil, DestroyTest, 4) +} + +// batches + +func BenchmarkGTreapIndexing1Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 1, 10) +} + +func BenchmarkGTreapIndexing2Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 2, 10) +} + +func BenchmarkGTreapIndexing4Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 4, 10) +} + +func BenchmarkGTreapIndexing1Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 1, 100) +} + +func BenchmarkGTreapIndexing2Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 2, 100) +} + +func BenchmarkGTreapIndexing4Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 4, 100) +} + +func BenchmarkGTreapIndexing1Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 1, 1000) +} + +func BenchmarkGTreapIndexing2Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 2, 1000) +} + +func BenchmarkGTreapIndexing4Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, gtreap.Name, nil, DestroyTest, 4, 1000) +} diff --git a/index/upsidedown/benchmark_null_test.go b/index/upsidedown/benchmark_null_test.go new file mode 100644 index 0000000..1576aae --- /dev/null +++ b/index/upsidedown/benchmark_null_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/index/upsidedown/store/null" +) + +func BenchmarkNullIndexing1Workers(b *testing.B) { + CommonBenchmarkIndex(b, null.Name, nil, DestroyTest, 1) +} + +func BenchmarkNullIndexing2Workers(b *testing.B) { + CommonBenchmarkIndex(b, null.Name, nil, DestroyTest, 2) +} + +func BenchmarkNullIndexing4Workers(b *testing.B) { + CommonBenchmarkIndex(b, null.Name, nil, DestroyTest, 4) +} + +// batches + +func BenchmarkNullIndexing1Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 1, 10) +} + +func BenchmarkNullIndexing2Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 2, 10) +} + +func BenchmarkNullIndexing4Workers10Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 4, 10) +} + +func BenchmarkNullIndexing1Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 1, 100) +} + +func BenchmarkNullIndexing2Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 2, 100) +} + +func BenchmarkNullIndexing4Workers100Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 4, 100) +} + +func BenchmarkNullIndexing1Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 1, 1000) +} + +func BenchmarkNullIndexing2Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 2, 1000) +} + +func BenchmarkNullIndexing4Workers1000Batch(b *testing.B) { + CommonBenchmarkIndexBatch(b, null.Name, nil, DestroyTest, 4, 1000) +} diff --git a/index/upsidedown/dump.go b/index/upsidedown/dump.go new file mode 100644 index 0000000..64ebb1b --- /dev/null +++ b/index/upsidedown/dump.go @@ -0,0 +1,174 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "bytes" + "sort" + + "github.com/blevesearch/upsidedown_store_api" +) + +// the functions in this file are only intended to be used by +// the bleve_dump utility and the debug http handlers +// if your application relies on them, you're doing something wrong +// they may change or be removed at any time + +func dumpPrefix(kvreader store.KVReader, rv chan interface{}, prefix []byte) { + start := prefix + if start == nil { + start = []byte{0} + } + it := kvreader.PrefixIterator(start) + defer func() { + cerr := it.Close() + if cerr != nil { + rv <- cerr + } + }() + key, val, valid := it.Current() + for valid { + ck := make([]byte, len(key)) + copy(ck, key) + cv := make([]byte, len(val)) + copy(cv, val) + row, err := ParseFromKeyValue(ck, cv) + if err != nil { + rv <- err + return + } + rv <- row + + it.Next() + key, val, valid = it.Current() + } +} + +func dumpRange(kvreader store.KVReader, rv chan interface{}, start, end []byte) { + it := kvreader.RangeIterator(start, end) + defer func() { + cerr := it.Close() + if cerr != nil { + rv <- cerr + } + }() + key, val, valid := it.Current() + for valid { + ck := make([]byte, len(key)) + copy(ck, key) + cv := make([]byte, len(val)) + copy(cv, val) + row, err := ParseFromKeyValue(ck, cv) + if err != nil { + rv <- err + return + } + rv <- row + + it.Next() + key, val, valid = it.Current() + } +} + +func (i *IndexReader) DumpAll() chan interface{} { + rv := make(chan interface{}) + go func() { + defer close(rv) + dumpRange(i.kvreader, rv, nil, nil) + }() + return rv +} + +func (i *IndexReader) DumpFields() chan interface{} { + rv := make(chan interface{}) + go func() { + defer close(rv) + dumpPrefix(i.kvreader, rv, []byte{'f'}) + }() + return rv +} + +type keyset [][]byte + +func (k keyset) Len() int { return len(k) } +func (k keyset) Swap(i, j int) { k[i], k[j] = k[j], k[i] } +func (k keyset) Less(i, j int) bool { return bytes.Compare(k[i], k[j]) < 0 } + +// DumpDoc returns all rows in the index related to this doc id +func (i *IndexReader) DumpDoc(id string) chan interface{} { + idBytes := []byte(id) + + rv := make(chan interface{}) + + go func() { + defer close(rv) + + back, err := backIndexRowForDoc(i.kvreader, []byte(id)) + if err != nil { + rv <- err + return + } + + // no such doc + if back == nil { + return + } + // build sorted list of term keys + keys := make(keyset, 0) + for _, entry := range back.termsEntries { + for i := range entry.Terms { + tfr := NewTermFrequencyRow([]byte(entry.Terms[i]), uint16(*entry.Field), idBytes, 0, 0) + key := tfr.Key() + keys = append(keys, key) + } + } + sort.Sort(keys) + + // first add all the stored rows + storedRowPrefix := NewStoredRow(idBytes, 0, []uint64{}, 'x', []byte{}).ScanPrefixForDoc() + dumpPrefix(i.kvreader, rv, storedRowPrefix) + + // now walk term keys in order and add them as well + if len(keys) > 0 { + it := i.kvreader.RangeIterator(keys[0], nil) + defer func() { + cerr := it.Close() + if cerr != nil { + rv <- cerr + } + }() + + for _, key := range keys { + it.Seek(key) + rkey, rval, valid := it.Current() + if !valid { + break + } + rck := make([]byte, len(rkey)) + copy(rck, key) + rcv := make([]byte, len(rval)) + copy(rcv, rval) + row, err := ParseFromKeyValue(rck, rcv) + if err != nil { + rv <- err + return + } + rv <- row + } + } + }() + + return rv +} diff --git a/index/upsidedown/dump_test.go b/index/upsidedown/dump_test.go new file mode 100644 index 0000000..35e00df --- /dev/null +++ b/index/upsidedown/dump_test.go @@ -0,0 +1,155 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "testing" + "time" + + "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + index "github.com/blevesearch/bleve_index_api" + + "github.com/blevesearch/bleve/v2/document" +) + +func TestDump(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 35.99, index.IndexField|index.StoreField)) + dateField, err := document.NewDateTimeFieldWithIndexingOptions("unixEpoch", []uint64{}, time.Unix(0, 0), time.RFC3339, index.IndexField|index.StoreField) + if err != nil { + t.Error(err) + } + doc.AddField(dateField) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("2") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test2"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 35.99, index.IndexField|index.StoreField)) + dateField, err = document.NewDateTimeFieldWithIndexingOptions("unixEpoch", []uint64{}, time.Unix(0, 0), time.RFC3339, index.IndexField|index.StoreField) + if err != nil { + t.Error(err) + } + doc.AddField(dateField) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + fieldsCount := 0 + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + upsideDownReader, ok := reader.(*IndexReader) + if !ok { + t.Fatal("dump is only supported by index type upsidedown") + } + fieldsRows := upsideDownReader.DumpFields() + for range fieldsRows { + fieldsCount++ + } + if fieldsCount != 3 { + t.Errorf("expected 3 fields, got %d", fieldsCount) + } + + // 1 text term + // 16 numeric terms + // 16 date terms + // 3 stored fields + expectedDocRowCount := int(1 + (2 * (64 / document.DefaultPrecisionStep)) + 3) + docRowCount := 0 + docRows := upsideDownReader.DumpDoc("1") + for range docRows { + docRowCount++ + } + if docRowCount != expectedDocRowCount { + t.Errorf("expected %d rows for document, got %d", expectedDocRowCount, docRowCount) + } + + docRowCount = 0 + docRows = upsideDownReader.DumpDoc("2") + for range docRows { + docRowCount++ + } + if docRowCount != expectedDocRowCount { + t.Errorf("expected %d rows for document, got %d", expectedDocRowCount, docRowCount) + } + + // 1 version + // fieldsCount field rows + // 2 docs * expectedDocRowCount + // 2 back index rows + // 2 text term row count (2 different text terms) + // 16 numeric term row counts (shared for both docs, same numeric value) + // 16 date term row counts (shared for both docs, same date value) + expectedAllRowCount := int(1 + fieldsCount + (2 * expectedDocRowCount) + 2 + 2 + int((2 * (64 / document.DefaultPrecisionStep)))) + allRowCount := 0 + allRows := upsideDownReader.DumpAll() + for range allRows { + allRowCount++ + } + if allRowCount != expectedAllRowCount { + t.Errorf("expected %d rows for all, got %d", expectedAllRowCount, allRowCount) + } + + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} diff --git a/index/upsidedown/field_cache.go b/index/upsidedown/field_cache.go new file mode 100644 index 0000000..1f68b71 --- /dev/null +++ b/index/upsidedown/field_cache.go @@ -0,0 +1,88 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "sync" +) + +type FieldCache struct { + fieldIndexes map[string]uint16 + indexFields []string + lastFieldIndex int + mutex sync.RWMutex +} + +func NewFieldCache() *FieldCache { + return &FieldCache{ + fieldIndexes: make(map[string]uint16), + lastFieldIndex: -1, + } +} + +func (f *FieldCache) AddExisting(field string, index uint16) { + f.mutex.Lock() + f.addLOCKED(field, index) + f.mutex.Unlock() +} + +func (f *FieldCache) addLOCKED(field string, index uint16) uint16 { + f.fieldIndexes[field] = index + if len(f.indexFields) < int(index)+1 { + prevIndexFields := f.indexFields + f.indexFields = make([]string, int(index)+16) + copy(f.indexFields, prevIndexFields) + } + f.indexFields[int(index)] = field + if int(index) > f.lastFieldIndex { + f.lastFieldIndex = int(index) + } + return index +} + +// FieldNamed returns the index of the field, and whether or not it existed +// before this call. if createIfMissing is true, and new field index is assigned +// but the second return value will still be false +func (f *FieldCache) FieldNamed(field string, createIfMissing bool) (uint16, bool) { + f.mutex.RLock() + if index, ok := f.fieldIndexes[field]; ok { + f.mutex.RUnlock() + return index, true + } else if !createIfMissing { + f.mutex.RUnlock() + return 0, false + } + // trade read lock for write lock + f.mutex.RUnlock() + f.mutex.Lock() + // need to check again with write lock + if index, ok := f.fieldIndexes[field]; ok { + f.mutex.Unlock() + return index, true + } + // assign next field id + index := f.addLOCKED(field, uint16(f.lastFieldIndex+1)) + f.mutex.Unlock() + return index, false +} + +func (f *FieldCache) FieldIndexed(index uint16) (field string) { + f.mutex.RLock() + if int(index) < len(f.indexFields) { + field = f.indexFields[int(index)] + } + f.mutex.RUnlock() + return field +} diff --git a/index/upsidedown/field_dict.go b/index/upsidedown/field_dict.go new file mode 100644 index 0000000..c990fd4 --- /dev/null +++ b/index/upsidedown/field_dict.go @@ -0,0 +1,86 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "fmt" + + index "github.com/blevesearch/bleve_index_api" + store "github.com/blevesearch/upsidedown_store_api" +) + +type UpsideDownCouchFieldDict struct { + indexReader *IndexReader + iterator store.KVIterator + dictRow *DictionaryRow + dictEntry *index.DictEntry + field uint16 +} + +func newUpsideDownCouchFieldDict(indexReader *IndexReader, field uint16, startTerm, endTerm []byte) (*UpsideDownCouchFieldDict, error) { + + startKey := NewDictionaryRow(startTerm, field, 0).Key() + if endTerm == nil { + endTerm = []byte{ByteSeparator} + } else { + endTerm = incrementBytes(endTerm) + } + endKey := NewDictionaryRow(endTerm, field, 0).Key() + + it := indexReader.kvreader.RangeIterator(startKey, endKey) + + return &UpsideDownCouchFieldDict{ + indexReader: indexReader, + iterator: it, + dictRow: &DictionaryRow{}, // Pre-alloced, reused row. + dictEntry: &index.DictEntry{}, // Pre-alloced, reused entry. + field: field, + }, nil + +} + +func (r *UpsideDownCouchFieldDict) BytesRead() uint64 { + return 0 +} + +func (r *UpsideDownCouchFieldDict) Next() (*index.DictEntry, error) { + key, val, valid := r.iterator.Current() + if !valid { + return nil, nil + } + + err := r.dictRow.parseDictionaryK(key) + if err != nil { + return nil, fmt.Errorf("unexpected error parsing dictionary row key: %v", err) + } + err = r.dictRow.parseDictionaryV(val) + if err != nil { + return nil, fmt.Errorf("unexpected error parsing dictionary row val: %v", err) + } + r.dictEntry.Term = string(r.dictRow.term) + r.dictEntry.Count = r.dictRow.count + // advance the iterator to the next term + r.iterator.Next() + return r.dictEntry, nil + +} + +func (r *UpsideDownCouchFieldDict) Cardinality() int { + return 0 +} + +func (r *UpsideDownCouchFieldDict) Close() error { + return r.iterator.Close() +} diff --git a/index/upsidedown/field_dict_test.go b/index/upsidedown/field_dict_test.go new file mode 100644 index 0000000..d796d7e --- /dev/null +++ b/index/upsidedown/field_dict_test.go @@ -0,0 +1,183 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + index "github.com/blevesearch/bleve_index_api" +) + +func TestIndexFieldDict(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("2") + doc.AddField(document.NewTextFieldWithAnalyzer("name", []uint64{}, []byte("test test test"), testAnalyzer)) + doc.AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("eat more rice"), index.IndexField|index.IncludeTermVectors, testAnalyzer)) + doc.AddField(document.NewTextFieldCustom("prefix", []uint64{}, []byte("bob cat cats catting dog doggy zoo"), index.IndexField|index.IncludeTermVectors, testAnalyzer)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + dict, err := indexReader.FieldDict("name") + if err != nil { + t.Errorf("error creating reader: %v", err) + } + defer func() { + err := dict.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount := 0 + curr, err := dict.Next() + for err == nil && curr != nil { + termCount++ + if curr.Term != "test" { + t.Errorf("expected term to be 'test', got '%s'", curr.Term) + } + curr, err = dict.Next() + } + if termCount != 1 { + t.Errorf("expected 1 term for this field, got %d", termCount) + } + + dict2, err := indexReader.FieldDict("desc") + if err != nil { + t.Errorf("error creating reader: %v", err) + } + defer func() { + err := dict2.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount = 0 + terms := make([]string, 0) + curr, err = dict2.Next() + for err == nil && curr != nil { + termCount++ + terms = append(terms, curr.Term) + curr, err = dict2.Next() + } + if termCount != 3 { + t.Errorf("expected 3 term for this field, got %d", termCount) + } + expectedTerms := []string{"eat", "more", "rice"} + if !reflect.DeepEqual(expectedTerms, terms) { + t.Errorf("expected %#v, got %#v", expectedTerms, terms) + } + + // test start and end range + dict3, err := indexReader.FieldDictRange("desc", []byte("fun"), []byte("nice")) + if err != nil { + t.Errorf("error creating reader: %v", err) + } + defer func() { + err := dict3.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount = 0 + terms = make([]string, 0) + curr, err = dict3.Next() + for err == nil && curr != nil { + termCount++ + terms = append(terms, curr.Term) + curr, err = dict3.Next() + } + if termCount != 1 { + t.Errorf("expected 1 term for this field, got %d", termCount) + } + expectedTerms = []string{"more"} + if !reflect.DeepEqual(expectedTerms, terms) { + t.Errorf("expected %#v, got %#v", expectedTerms, terms) + } + + // test use case for prefix + dict4, err := indexReader.FieldDictPrefix("prefix", []byte("cat")) + if err != nil { + t.Errorf("error creating reader: %v", err) + } + defer func() { + err := dict4.Close() + if err != nil { + t.Fatal(err) + } + }() + + termCount = 0 + terms = make([]string, 0) + curr, err = dict4.Next() + for err == nil && curr != nil { + termCount++ + terms = append(terms, curr.Term) + curr, err = dict4.Next() + } + if termCount != 3 { + t.Errorf("expected 3 term for this field, got %d", termCount) + } + expectedTerms = []string{"cat", "cats", "catting"} + if !reflect.DeepEqual(expectedTerms, terms) { + t.Errorf("expected %#v, got %#v", expectedTerms, terms) + } +} diff --git a/index/upsidedown/index_reader.go b/index/upsidedown/index_reader.go new file mode 100644 index 0000000..0b91739 --- /dev/null +++ b/index/upsidedown/index_reader.go @@ -0,0 +1,228 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "context" + "reflect" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" + store "github.com/blevesearch/upsidedown_store_api" +) + +var reflectStaticSizeIndexReader int + +func init() { + var ir IndexReader + reflectStaticSizeIndexReader = int(reflect.TypeOf(ir).Size()) +} + +type IndexReader struct { + index *UpsideDownCouch + kvreader store.KVReader + docCount uint64 +} + +func (i *IndexReader) TermFieldReader(ctx context.Context, term []byte, fieldName string, includeFreq, includeNorm, includeTermVectors bool) (index.TermFieldReader, error) { + fieldIndex, fieldExists := i.index.fieldCache.FieldNamed(fieldName, false) + if fieldExists { + return newUpsideDownCouchTermFieldReader(i, term, uint16(fieldIndex), includeFreq, includeNorm, includeTermVectors) + } + return newUpsideDownCouchTermFieldReader(i, []byte{ByteSeparator}, ^uint16(0), includeFreq, includeNorm, includeTermVectors) +} + +func (i *IndexReader) FieldDict(fieldName string) (index.FieldDict, error) { + return i.FieldDictRange(fieldName, nil, nil) +} + +func (i *IndexReader) FieldDictRange(fieldName string, startTerm []byte, endTerm []byte) (index.FieldDict, error) { + fieldIndex, fieldExists := i.index.fieldCache.FieldNamed(fieldName, false) + if fieldExists { + return newUpsideDownCouchFieldDict(i, uint16(fieldIndex), startTerm, endTerm) + } + return newUpsideDownCouchFieldDict(i, ^uint16(0), []byte{ByteSeparator}, []byte{}) +} + +func (i *IndexReader) FieldDictPrefix(fieldName string, termPrefix []byte) (index.FieldDict, error) { + return i.FieldDictRange(fieldName, termPrefix, termPrefix) +} + +func (i *IndexReader) DocIDReaderAll() (index.DocIDReader, error) { + return newUpsideDownCouchDocIDReader(i) +} + +func (i *IndexReader) DocIDReaderOnly(ids []string) (index.DocIDReader, error) { + return newUpsideDownCouchDocIDReaderOnly(i, ids) +} + +func (i *IndexReader) Document(id string) (doc index.Document, err error) { + // first hit the back index to confirm doc exists + var backIndexRow *BackIndexRow + backIndexRow, err = backIndexRowForDoc(i.kvreader, []byte(id)) + if err != nil { + return + } + if backIndexRow == nil { + return + } + rvd := document.NewDocument(id) + storedRow := NewStoredRow([]byte(id), 0, []uint64{}, 'x', nil) + storedRowScanPrefix := storedRow.ScanPrefixForDoc() + it := i.kvreader.PrefixIterator(storedRowScanPrefix) + defer func() { + if cerr := it.Close(); err == nil && cerr != nil { + err = cerr + } + }() + key, val, valid := it.Current() + for valid { + safeVal := make([]byte, len(val)) + copy(safeVal, val) + var row *StoredRow + row, err = NewStoredRowKV(key, safeVal) + if err != nil { + return nil, err + } + if row != nil { + fieldName := i.index.fieldCache.FieldIndexed(row.field) + field := decodeFieldType(row.typ, fieldName, row.arrayPositions, row.value) + if field != nil { + rvd.AddField(field) + } + } + + it.Next() + key, val, valid = it.Current() + } + return rvd, nil +} + +func (i *IndexReader) documentVisitFieldTerms(id index.IndexInternalID, fields []string, visitor index.DocValueVisitor) error { + fieldsMap := make(map[uint16]string, len(fields)) + for _, f := range fields { + id, ok := i.index.fieldCache.FieldNamed(f, false) + if ok { + fieldsMap[id] = f + } + } + + tempRow := BackIndexRow{ + doc: id, + } + + keyBuf := GetRowBuffer() + if tempRow.KeySize() > len(keyBuf.buf) { + keyBuf.buf = make([]byte, 2*tempRow.KeySize()) + } + defer PutRowBuffer(keyBuf) + keySize, err := tempRow.KeyTo(keyBuf.buf) + if err != nil { + return err + } + + value, err := i.kvreader.Get(keyBuf.buf[:keySize]) + if err != nil { + return err + } + if value == nil { + return nil + } + + return visitBackIndexRow(value, func(field uint32, term []byte) { + if field, ok := fieldsMap[uint16(field)]; ok { + visitor(field, term) + } + }) +} + +func (i *IndexReader) Fields() (fields []string, err error) { + fields = make([]string, 0) + it := i.kvreader.PrefixIterator([]byte{'f'}) + defer func() { + if cerr := it.Close(); err == nil && cerr != nil { + err = cerr + } + }() + key, val, valid := it.Current() + for valid { + var row UpsideDownCouchRow + row, err = ParseFromKeyValue(key, val) + if err != nil { + fields = nil + return + } + if row != nil { + fieldRow, ok := row.(*FieldRow) + if ok { + fields = append(fields, fieldRow.name) + } + } + + it.Next() + key, val, valid = it.Current() + } + return +} + +func (i *IndexReader) GetInternal(key []byte) ([]byte, error) { + internalRow := NewInternalRow(key, nil) + return i.kvreader.Get(internalRow.Key()) +} + +func (i *IndexReader) DocCount() (uint64, error) { + return i.docCount, nil +} + +func (i *IndexReader) Close() error { + return i.kvreader.Close() +} + +func (i *IndexReader) ExternalID(id index.IndexInternalID) (string, error) { + return string(id), nil +} + +func (i *IndexReader) InternalID(id string) (index.IndexInternalID, error) { + return index.IndexInternalID(id), nil +} + +func incrementBytes(in []byte) []byte { + rv := make([]byte, len(in)) + copy(rv, in) + for i := len(rv) - 1; i >= 0; i-- { + rv[i] = rv[i] + 1 + if rv[i] != 0 { + // didn't overflow, so stop + break + } + } + return rv +} + +func (i *IndexReader) DocValueReader(fields []string) (index.DocValueReader, error) { + return &DocValueReader{i: i, fields: fields}, nil +} + +type DocValueReader struct { + i *IndexReader + fields []string +} + +func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID, + visitor index.DocValueVisitor) error { + return dvr.i.documentVisitFieldTerms(id, dvr.fields, visitor) +} + +func (dvr *DocValueReader) BytesRead() uint64 { return 0 } diff --git a/index/upsidedown/reader.go b/index/upsidedown/reader.go new file mode 100644 index 0000000..68b1531 --- /dev/null +++ b/index/upsidedown/reader.go @@ -0,0 +1,376 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "bytes" + "reflect" + "sort" + "sync/atomic" + + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" + "github.com/blevesearch/upsidedown_store_api" +) + +var reflectStaticSizeUpsideDownCouchTermFieldReader int +var reflectStaticSizeUpsideDownCouchDocIDReader int + +func init() { + var tfr UpsideDownCouchTermFieldReader + reflectStaticSizeUpsideDownCouchTermFieldReader = + int(reflect.TypeOf(tfr).Size()) + var cdr UpsideDownCouchDocIDReader + reflectStaticSizeUpsideDownCouchDocIDReader = + int(reflect.TypeOf(cdr).Size()) +} + +type UpsideDownCouchTermFieldReader struct { + count uint64 + indexReader *IndexReader + iterator store.KVIterator + term []byte + tfrNext *TermFrequencyRow + tfrPrealloc TermFrequencyRow + keyBuf []byte + field uint16 + includeTermVectors bool +} + +func (r *UpsideDownCouchTermFieldReader) Size() int { + sizeInBytes := reflectStaticSizeUpsideDownCouchTermFieldReader + size.SizeOfPtr + + len(r.term) + + r.tfrPrealloc.Size() + + len(r.keyBuf) + + if r.tfrNext != nil { + sizeInBytes += r.tfrNext.Size() + } + + return sizeInBytes +} + +func newUpsideDownCouchTermFieldReader(indexReader *IndexReader, term []byte, field uint16, includeFreq, includeNorm, includeTermVectors bool) (*UpsideDownCouchTermFieldReader, error) { + bufNeeded := termFrequencyRowKeySize(term, nil) + if bufNeeded < dictionaryRowKeySize(term) { + bufNeeded = dictionaryRowKeySize(term) + } + buf := make([]byte, bufNeeded) + + bufUsed := dictionaryRowKeyTo(buf, field, term) + val, err := indexReader.kvreader.Get(buf[:bufUsed]) + if err != nil { + return nil, err + } + if val == nil { + atomic.AddUint64(&indexReader.index.stats.termSearchersStarted, uint64(1)) + rv := &UpsideDownCouchTermFieldReader{ + count: 0, + term: term, + field: field, + includeTermVectors: includeTermVectors, + } + rv.tfrNext = &rv.tfrPrealloc + return rv, nil + } + + count, err := dictionaryRowParseV(val) + if err != nil { + return nil, err + } + + bufUsed = termFrequencyRowKeyTo(buf, field, term, nil) + it := indexReader.kvreader.PrefixIterator(buf[:bufUsed]) + + atomic.AddUint64(&indexReader.index.stats.termSearchersStarted, uint64(1)) + return &UpsideDownCouchTermFieldReader{ + indexReader: indexReader, + iterator: it, + count: count, + term: term, + field: field, + includeTermVectors: includeTermVectors, + }, nil +} + +func (r *UpsideDownCouchTermFieldReader) Count() uint64 { + return r.count +} + +func (r *UpsideDownCouchTermFieldReader) Next(preAlloced *index.TermFieldDoc) (*index.TermFieldDoc, error) { + if r.iterator != nil { + // We treat tfrNext also like an initialization flag, which + // tells us whether we need to invoke the underlying + // iterator.Next(). The first time, don't call iterator.Next(). + if r.tfrNext != nil { + r.iterator.Next() + } else { + r.tfrNext = &r.tfrPrealloc + } + key, val, valid := r.iterator.Current() + if valid { + tfr := r.tfrNext + err := tfr.parseKDoc(key, r.term) + if err != nil { + return nil, err + } + err = tfr.parseV(val, r.includeTermVectors) + if err != nil { + return nil, err + } + rv := preAlloced + if rv == nil { + rv = &index.TermFieldDoc{} + } + rv.ID = append(rv.ID, tfr.doc...) + rv.Freq = tfr.freq + rv.Norm = float64(tfr.norm) + if tfr.vectors != nil { + rv.Vectors = r.indexReader.index.termFieldVectorsFromTermVectors(tfr.vectors) + } + return rv, nil + } + } + return nil, nil +} + +func (r *UpsideDownCouchTermFieldReader) Advance(docID index.IndexInternalID, preAlloced *index.TermFieldDoc) (rv *index.TermFieldDoc, err error) { + if r.iterator != nil { + if r.tfrNext == nil { + r.tfrNext = &TermFrequencyRow{} + } + tfr := InitTermFrequencyRow(r.tfrNext, r.term, r.field, docID, 0, 0) + r.keyBuf, err = tfr.KeyAppendTo(r.keyBuf[:0]) + if err != nil { + return nil, err + } + r.iterator.Seek(r.keyBuf) + key, val, valid := r.iterator.Current() + if valid { + err := tfr.parseKDoc(key, r.term) + if err != nil { + return nil, err + } + err = tfr.parseV(val, r.includeTermVectors) + if err != nil { + return nil, err + } + rv = preAlloced + if rv == nil { + rv = &index.TermFieldDoc{} + } + rv.ID = append(rv.ID, tfr.doc...) + rv.Freq = tfr.freq + rv.Norm = float64(tfr.norm) + if tfr.vectors != nil { + rv.Vectors = r.indexReader.index.termFieldVectorsFromTermVectors(tfr.vectors) + } + return rv, nil + } + } + return nil, nil +} + +func (r *UpsideDownCouchTermFieldReader) Close() error { + if r.indexReader != nil { + atomic.AddUint64(&r.indexReader.index.stats.termSearchersFinished, uint64(1)) + } + if r.iterator != nil { + return r.iterator.Close() + } + return nil +} + +type UpsideDownCouchDocIDReader struct { + indexReader *IndexReader + iterator store.KVIterator + only []string + onlyPos int + onlyMode bool +} + +func (r *UpsideDownCouchDocIDReader) Size() int { + sizeInBytes := reflectStaticSizeUpsideDownCouchDocIDReader + + reflectStaticSizeIndexReader + size.SizeOfPtr + + for _, entry := range r.only { + sizeInBytes += size.SizeOfString + len(entry) + } + + return sizeInBytes +} + +func newUpsideDownCouchDocIDReader(indexReader *IndexReader) (*UpsideDownCouchDocIDReader, error) { + startBytes := []byte{0x0} + endBytes := []byte{0xff} + + bisr := NewBackIndexRow(startBytes, nil, nil) + bier := NewBackIndexRow(endBytes, nil, nil) + it := indexReader.kvreader.RangeIterator(bisr.Key(), bier.Key()) + + return &UpsideDownCouchDocIDReader{ + indexReader: indexReader, + iterator: it, + }, nil +} + +func newUpsideDownCouchDocIDReaderOnly(indexReader *IndexReader, ids []string) (*UpsideDownCouchDocIDReader, error) { + // we don't actually own the list of ids, so if before we sort we must copy + idsCopy := make([]string, len(ids)) + copy(idsCopy, ids) + // ensure ids are sorted + sort.Strings(idsCopy) + startBytes := []byte{0x0} + if len(idsCopy) > 0 { + startBytes = []byte(idsCopy[0]) + } + endBytes := []byte{0xff} + if len(idsCopy) > 0 { + endBytes = incrementBytes([]byte(idsCopy[len(idsCopy)-1])) + } + bisr := NewBackIndexRow(startBytes, nil, nil) + bier := NewBackIndexRow(endBytes, nil, nil) + it := indexReader.kvreader.RangeIterator(bisr.Key(), bier.Key()) + + return &UpsideDownCouchDocIDReader{ + indexReader: indexReader, + iterator: it, + only: idsCopy, + onlyMode: true, + }, nil +} + +func (r *UpsideDownCouchDocIDReader) Next() (index.IndexInternalID, error) { + key, val, valid := r.iterator.Current() + + if r.onlyMode { + var rv index.IndexInternalID + for valid && r.onlyPos < len(r.only) { + br, err := NewBackIndexRowKV(key, val) + if err != nil { + return nil, err + } + if !bytes.Equal(br.doc, []byte(r.only[r.onlyPos])) { + ok := r.nextOnly() + if !ok { + return nil, nil + } + r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key()) + key, val, valid = r.iterator.Current() + continue + } else { + rv = append([]byte(nil), br.doc...) + break + } + } + if valid && r.onlyPos < len(r.only) { + ok := r.nextOnly() + if ok { + r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key()) + } + return rv, nil + } + + } else { + if valid { + br, err := NewBackIndexRowKV(key, val) + if err != nil { + return nil, err + } + rv := append([]byte(nil), br.doc...) + r.iterator.Next() + return rv, nil + } + } + return nil, nil +} + +func (r *UpsideDownCouchDocIDReader) Advance(docID index.IndexInternalID) (index.IndexInternalID, error) { + + if r.onlyMode { + r.onlyPos = sort.SearchStrings(r.only, string(docID)) + if r.onlyPos >= len(r.only) { + // advanced to key after our last only key + return nil, nil + } + r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key()) + key, val, valid := r.iterator.Current() + + var rv index.IndexInternalID + for valid && r.onlyPos < len(r.only) { + br, err := NewBackIndexRowKV(key, val) + if err != nil { + return nil, err + } + if !bytes.Equal(br.doc, []byte(r.only[r.onlyPos])) { + // the only key we seek'd to didn't exist + // now look for the closest key that did exist in only + r.onlyPos = sort.SearchStrings(r.only, string(br.doc)) + if r.onlyPos >= len(r.only) { + // advanced to key after our last only key + return nil, nil + } + // now seek to this new only key + r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key()) + key, val, valid = r.iterator.Current() + continue + } else { + rv = append([]byte(nil), br.doc...) + break + } + } + if valid && r.onlyPos < len(r.only) { + ok := r.nextOnly() + if ok { + r.iterator.Seek(NewBackIndexRow([]byte(r.only[r.onlyPos]), nil, nil).Key()) + } + return rv, nil + } + } else { + bir := NewBackIndexRow(docID, nil, nil) + r.iterator.Seek(bir.Key()) + key, val, valid := r.iterator.Current() + if valid { + br, err := NewBackIndexRowKV(key, val) + if err != nil { + return nil, err + } + rv := append([]byte(nil), br.doc...) + r.iterator.Next() + return rv, nil + } + } + return nil, nil +} + +func (r *UpsideDownCouchDocIDReader) Close() error { + return r.iterator.Close() +} + +// move the r.only pos forward one, skipping duplicates +// return true if there is more data, or false if we got to the end of the list +func (r *UpsideDownCouchDocIDReader) nextOnly() bool { + + // advance 1 position, until we see a different key + // it's already sorted, so this skips duplicates + start := r.onlyPos + r.onlyPos++ + for r.onlyPos < len(r.only) && r.only[r.onlyPos] == r.only[start] { + start = r.onlyPos + r.onlyPos++ + } + // inidicate if we got to the end of the list + return r.onlyPos < len(r.only) +} diff --git a/index/upsidedown/reader_test.go b/index/upsidedown/reader_test.go new file mode 100644 index 0000000..cec2eca --- /dev/null +++ b/index/upsidedown/reader_test.go @@ -0,0 +1,548 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + index "github.com/blevesearch/bleve_index_api" +) + +func TestIndexReader(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextFieldWithAnalyzer("name", []uint64{}, []byte("test test test"), testAnalyzer)) + doc.AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("eat more rice"), index.IndexField|index.IncludeTermVectors, testAnalyzer)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + // first look for a term that doesn't exist + reader, err := indexReader.TermFieldReader(context.TODO(), []byte("nope"), "name", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + count := reader.Count() + if count != 0 { + t.Errorf("Expected doc count to be: %d got: %d", 0, count) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + reader, err = indexReader.TermFieldReader(context.TODO(), []byte("test"), "name", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + + count = reader.Count() + if count != expectedCount { + t.Errorf("Expected doc count to be: %d got: %d", expectedCount, count) + } + + var match *index.TermFieldDoc + var actualCount uint64 + match, err = reader.Next(nil) + for err == nil && match != nil { + match, err = reader.Next(nil) + if err != nil { + t.Errorf("unexpected error reading next") + } + actualCount++ + } + if actualCount != count { + t.Errorf("count was 2, but only saw %d", actualCount) + } + + expectedMatch := &index.TermFieldDoc{ + ID: index.IndexInternalID("2"), + Freq: 1, + Norm: 0.5773502588272095, + Vectors: []*index.TermFieldVector{ + { + Field: "desc", + Pos: 3, + Start: 9, + End: 13, + }, + }, + } + tfr, err := indexReader.TermFieldReader(context.TODO(), []byte("rice"), "desc", true, true, true) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + match, err = tfr.Next(nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(expectedMatch, match) { + t.Errorf("got %#v, expected %#v", match, expectedMatch) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now test usage of advance + reader, err = indexReader.TermFieldReader(context.TODO(), []byte("test"), "name", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + + match, err = reader.Advance(index.IndexInternalID("2"), nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match == nil { + t.Fatalf("Expected match, got nil") + } + if !match.ID.Equals(index.IndexInternalID("2")) { + t.Errorf("Expected ID '2', got '%s'", match.ID) + } + match, err = reader.Advance(index.IndexInternalID("3"), nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match != nil { + t.Errorf("expected nil, got %v", match) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // now test creating a reader for a field that doesn't exist + reader, err = indexReader.TermFieldReader(context.TODO(), []byte("water"), "doesnotexist", true, true, true) + if err != nil { + t.Errorf("Error accessing term field reader: %v", err) + } + count = reader.Count() + if count != 0 { + t.Errorf("expected count 0 for reader of non-existent field") + } + match, err = reader.Next(nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match != nil { + t.Errorf("expected nil, got %v", match) + } + match, err = reader.Advance(index.IndexInternalID("anywhere"), nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match != nil { + t.Errorf("expected nil, got %v", match) + } +} + +func TestIndexDocIdReader(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test test test"))) + doc.AddField(document.NewTextFieldWithIndexingOptions("desc", []uint64{}, []byte("eat more rice"), index.IndexField|index.IncludeTermVectors)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Error(err) + } + }() + + // first get all doc ids + reader, err := indexReader.DocIDReaderAll() + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader.Close() + if err != nil { + t.Fatal(err) + } + }() + + id, err := reader.Next() + if err != nil { + t.Fatal(err) + } + + count := uint64(0) + for id != nil { + count++ + id, err = reader.Next() + if err != nil { + t.Fatal(err) + } + } + if count != expectedCount { + t.Errorf("expected %d, got %d", expectedCount, count) + } + + // try it again, but jump to the second doc this time + reader2, err := indexReader.DocIDReaderAll() + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader2.Close() + if err != nil { + t.Error(err) + } + }() + + id, err = reader2.Advance(index.IndexInternalID("2")) + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("2")) { + t.Errorf("expected to find id '2', got '%s'", id) + } + + id, err = reader2.Advance(index.IndexInternalID("3")) + if err != nil { + t.Error(err) + } + if id != nil { + t.Errorf("expected to find id '', got '%s'", id) + } +} + +func TestCrashBadBackIndexRow(t *testing.T) { + br, err := NewBackIndexRowKV([]byte{byte('b'), byte('a'), ByteSeparator}, []byte{}) + if err != nil { + t.Fatal(err) + } + if string(br.doc) != "a" { + t.Fatal(err) + } +} + +func TestIndexDocIdOnlyReader(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("3") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("5") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("7") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + doc = document.NewDocument("9") + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Error(err) + } + }() + + onlyIds := []string{"1", "5", "9"} + reader, err := indexReader.DocIDReaderOnly(onlyIds) + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader.Close() + if err != nil { + t.Fatal(err) + } + }() + + id, err := reader.Next() + if err != nil { + t.Fatal(err) + } + + count := uint64(0) + for id != nil { + count++ + id, err = reader.Next() + if err != nil { + t.Fatal(err) + } + } + if count != 3 { + t.Errorf("expected 3, got %d", count) + } + + // try it again, but jump + reader2, err := indexReader.DocIDReaderOnly(onlyIds) + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader2.Close() + if err != nil { + t.Error(err) + } + }() + + id, err = reader2.Advance(index.IndexInternalID("5")) + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("5")) { + t.Errorf("expected to find id '5', got '%s'", id) + } + + id, err = reader2.Advance(index.IndexInternalID("a")) + if err != nil { + t.Error(err) + } + if id != nil { + t.Errorf("expected to find id '', got '%s'", id) + } + + // some keys aren't actually there + onlyIds = []string{"0", "2", "4", "5", "6", "8", "a"} + reader3, err := indexReader.DocIDReaderOnly(onlyIds) + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader3.Close() + if err != nil { + t.Error(err) + } + }() + + id, err = reader3.Next() + if err != nil { + t.Fatal(err) + } + + count = uint64(0) + for id != nil { + count++ + id, err = reader3.Next() + if err != nil { + t.Fatal(err) + } + } + if count != 1 { + t.Errorf("expected 1, got %d", count) + } + + // mix advance and next + onlyIds = []string{"0", "1", "3", "5", "6", "9"} + reader4, err := indexReader.DocIDReaderOnly(onlyIds) + if err != nil { + t.Errorf("Error accessing doc id reader: %v", err) + } + defer func() { + err := reader4.Close() + if err != nil { + t.Error(err) + } + }() + + // first key is "1" + id, err = reader4.Next() + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("1")) { + t.Errorf("expected to find id '1', got '%s'", id) + } + + // advancing to key we dont have gives next + id, err = reader4.Advance(index.IndexInternalID("2")) + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("3")) { + t.Errorf("expected to find id '3', got '%s'", id) + } + + // next after advance works + id, err = reader4.Next() + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("5")) { + t.Errorf("expected to find id '5', got '%s'", id) + } + + // advancing to key we do have works + id, err = reader4.Advance(index.IndexInternalID("9")) + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("9")) { + t.Errorf("expected to find id '9', got '%s'", id) + } + + // advance backwards at end + id, err = reader4.Advance(index.IndexInternalID("4")) + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("5")) { + t.Errorf("expected to find id '5', got '%s'", id) + } + + // next after advance works + id, err = reader4.Next() + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("9")) { + t.Errorf("expected to find id '9', got '%s'", id) + } + + // advance backwards to key that exists, but not in only set + id, err = reader4.Advance(index.IndexInternalID("7")) + if err != nil { + t.Error(err) + } + if !id.Equals(index.IndexInternalID("9")) { + t.Errorf("expected to find id '9', got '%s'", id) + } +} diff --git a/index/upsidedown/row.go b/index/upsidedown/row.go new file mode 100644 index 0000000..622db46 --- /dev/null +++ b/index/upsidedown/row.go @@ -0,0 +1,1144 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/size" + "github.com/golang/protobuf/proto" +) + +var ( + reflectStaticSizeTermFrequencyRow int + reflectStaticSizeTermVector int +) + +func init() { + var tfr TermFrequencyRow + reflectStaticSizeTermFrequencyRow = int(reflect.TypeOf(tfr).Size()) + var tv TermVector + reflectStaticSizeTermVector = int(reflect.TypeOf(tv).Size()) +} + +const ByteSeparator byte = 0xff + +type UpsideDownCouchRowStream chan UpsideDownCouchRow + +type UpsideDownCouchRow interface { + KeySize() int + KeyTo([]byte) (int, error) + Key() []byte + Value() []byte + ValueSize() int + ValueTo([]byte) (int, error) +} + +func ParseFromKeyValue(key, value []byte) (UpsideDownCouchRow, error) { + if len(key) > 0 { + switch key[0] { + case 'v': + return NewVersionRowKV(key, value) + case 'f': + return NewFieldRowKV(key, value) + case 'd': + return NewDictionaryRowKV(key, value) + case 't': + return NewTermFrequencyRowKV(key, value) + case 'b': + return NewBackIndexRowKV(key, value) + case 's': + return NewStoredRowKV(key, value) + case 'i': + return NewInternalRowKV(key, value) + } + return nil, fmt.Errorf("Unknown field type '%s'", string(key[0])) + } + return nil, fmt.Errorf("Invalid empty key") +} + +// VERSION + +type VersionRow struct { + version uint8 +} + +func (v *VersionRow) Key() []byte { + return []byte{'v'} +} + +func (v *VersionRow) KeySize() int { + return 1 +} + +func (v *VersionRow) KeyTo(buf []byte) (int, error) { + buf[0] = 'v' + return 1, nil +} + +func (v *VersionRow) Value() []byte { + return []byte{byte(v.version)} +} + +func (v *VersionRow) ValueSize() int { + return 1 +} + +func (v *VersionRow) ValueTo(buf []byte) (int, error) { + buf[0] = v.version + return 1, nil +} + +func (v *VersionRow) String() string { + return fmt.Sprintf("Version: %d", v.version) +} + +func NewVersionRow(version uint8) *VersionRow { + return &VersionRow{ + version: version, + } +} + +func NewVersionRowKV(key, value []byte) (*VersionRow, error) { + rv := VersionRow{} + buf := bytes.NewBuffer(value) + err := binary.Read(buf, binary.LittleEndian, &rv.version) + if err != nil { + return nil, err + } + return &rv, nil +} + +// INTERNAL STORAGE + +type InternalRow struct { + key []byte + val []byte +} + +func (i *InternalRow) Key() []byte { + buf := make([]byte, i.KeySize()) + size, _ := i.KeyTo(buf) + return buf[:size] +} + +func (i *InternalRow) KeySize() int { + return len(i.key) + 1 +} + +func (i *InternalRow) KeyTo(buf []byte) (int, error) { + buf[0] = 'i' + actual := copy(buf[1:], i.key) + return 1 + actual, nil +} + +func (i *InternalRow) Value() []byte { + return i.val +} + +func (i *InternalRow) ValueSize() int { + return len(i.val) +} + +func (i *InternalRow) ValueTo(buf []byte) (int, error) { + actual := copy(buf, i.val) + return actual, nil +} + +func (i *InternalRow) String() string { + return fmt.Sprintf("InternalStore - Key: %s (% x) Val: %s (% x)", i.key, i.key, i.val, i.val) +} + +func NewInternalRow(key, val []byte) *InternalRow { + return &InternalRow{ + key: key, + val: val, + } +} + +func NewInternalRowKV(key, value []byte) (*InternalRow, error) { + rv := InternalRow{} + rv.key = key[1:] + rv.val = value + return &rv, nil +} + +// FIELD definition + +type FieldRow struct { + index uint16 + name string +} + +func (f *FieldRow) Key() []byte { + buf := make([]byte, f.KeySize()) + size, _ := f.KeyTo(buf) + return buf[:size] +} + +func (f *FieldRow) KeySize() int { + return 3 +} + +func (f *FieldRow) KeyTo(buf []byte) (int, error) { + buf[0] = 'f' + binary.LittleEndian.PutUint16(buf[1:3], f.index) + return 3, nil +} + +func (f *FieldRow) Value() []byte { + return append([]byte(f.name), ByteSeparator) +} + +func (f *FieldRow) ValueSize() int { + return len(f.name) + 1 +} + +func (f *FieldRow) ValueTo(buf []byte) (int, error) { + size := copy(buf, f.name) + buf[size] = ByteSeparator + return size + 1, nil +} + +func (f *FieldRow) String() string { + return fmt.Sprintf("Field: %d Name: %s", f.index, f.name) +} + +func NewFieldRow(index uint16, name string) *FieldRow { + return &FieldRow{ + index: index, + name: name, + } +} + +func NewFieldRowKV(key, value []byte) (*FieldRow, error) { + rv := FieldRow{} + + buf := bytes.NewBuffer(key) + _, err := buf.ReadByte() // type + if err != nil { + return nil, err + } + err = binary.Read(buf, binary.LittleEndian, &rv.index) + if err != nil { + return nil, err + } + + buf = bytes.NewBuffer(value) + rv.name, err = buf.ReadString(ByteSeparator) + if err != nil { + return nil, err + } + rv.name = rv.name[:len(rv.name)-1] // trim off separator byte + + return &rv, nil +} + +// DICTIONARY + +const DictionaryRowMaxValueSize = binary.MaxVarintLen64 + +type DictionaryRow struct { + term []byte + count uint64 + field uint16 +} + +func (dr *DictionaryRow) Key() []byte { + buf := make([]byte, dr.KeySize()) + size, _ := dr.KeyTo(buf) + return buf[:size] +} + +func (dr *DictionaryRow) KeySize() int { + return dictionaryRowKeySize(dr.term) +} + +func dictionaryRowKeySize(term []byte) int { + return len(term) + 3 +} + +func (dr *DictionaryRow) KeyTo(buf []byte) (int, error) { + return dictionaryRowKeyTo(buf, dr.field, dr.term), nil +} + +func dictionaryRowKeyTo(buf []byte, field uint16, term []byte) int { + buf[0] = 'd' + binary.LittleEndian.PutUint16(buf[1:3], field) + size := copy(buf[3:], term) + return size + 3 +} + +func (dr *DictionaryRow) Value() []byte { + buf := make([]byte, dr.ValueSize()) + size, _ := dr.ValueTo(buf) + return buf[:size] +} + +func (dr *DictionaryRow) ValueSize() int { + return DictionaryRowMaxValueSize +} + +func (dr *DictionaryRow) ValueTo(buf []byte) (int, error) { + used := binary.PutUvarint(buf, dr.count) + return used, nil +} + +func (dr *DictionaryRow) String() string { + return fmt.Sprintf("Dictionary Term: `%s` Field: %d Count: %d ", string(dr.term), dr.field, dr.count) +} + +func NewDictionaryRow(term []byte, field uint16, count uint64) *DictionaryRow { + return &DictionaryRow{ + term: term, + field: field, + count: count, + } +} + +func NewDictionaryRowKV(key, value []byte) (*DictionaryRow, error) { + rv, err := NewDictionaryRowK(key) + if err != nil { + return nil, err + } + + err = rv.parseDictionaryV(value) + if err != nil { + return nil, err + } + return rv, nil +} + +func NewDictionaryRowK(key []byte) (*DictionaryRow, error) { + rv := &DictionaryRow{} + err := rv.parseDictionaryK(key) + if err != nil { + return nil, err + } + return rv, nil +} + +func (dr *DictionaryRow) parseDictionaryK(key []byte) error { + dr.field = binary.LittleEndian.Uint16(key[1:3]) + if dr.term != nil { + dr.term = dr.term[:0] + } + dr.term = append(dr.term, key[3:]...) + return nil +} + +func (dr *DictionaryRow) parseDictionaryV(value []byte) error { + count, err := dictionaryRowParseV(value) + if err != nil { + return err + } + dr.count = count + return nil +} + +func dictionaryRowParseV(value []byte) (uint64, error) { + count, nread := binary.Uvarint(value) + if nread <= 0 { + return 0, fmt.Errorf("DictionaryRow parse Uvarint error, nread: %d", nread) + } + return count, nil +} + +// TERM FIELD FREQUENCY + +type TermVector struct { + field uint16 + arrayPositions []uint64 + pos uint64 + start uint64 + end uint64 +} + +func (tv *TermVector) Size() int { + return reflectStaticSizeTermVector + size.SizeOfPtr + + len(tv.arrayPositions)*size.SizeOfUint64 +} + +func (tv *TermVector) String() string { + return fmt.Sprintf("Field: %d Pos: %d Start: %d End %d ArrayPositions: %#v", tv.field, tv.pos, tv.start, tv.end, tv.arrayPositions) +} + +type TermFrequencyRow struct { + term []byte + doc []byte + freq uint64 + vectors []*TermVector + norm float32 + field uint16 +} + +func (tfr *TermFrequencyRow) Size() int { + sizeInBytes := reflectStaticSizeTermFrequencyRow + + len(tfr.term) + + len(tfr.doc) + + for _, entry := range tfr.vectors { + sizeInBytes += entry.Size() + } + + return sizeInBytes +} + +func (tfr *TermFrequencyRow) Term() []byte { + return tfr.term +} + +func (tfr *TermFrequencyRow) Freq() uint64 { + return tfr.freq +} + +func (tfr *TermFrequencyRow) ScanPrefixForField() []byte { + buf := make([]byte, 3) + buf[0] = 't' + binary.LittleEndian.PutUint16(buf[1:3], tfr.field) + return buf +} + +func (tfr *TermFrequencyRow) ScanPrefixForFieldTermPrefix() []byte { + buf := make([]byte, 3+len(tfr.term)) + buf[0] = 't' + binary.LittleEndian.PutUint16(buf[1:3], tfr.field) + copy(buf[3:], tfr.term) + return buf +} + +func (tfr *TermFrequencyRow) ScanPrefixForFieldTerm() []byte { + buf := make([]byte, 3+len(tfr.term)+1) + buf[0] = 't' + binary.LittleEndian.PutUint16(buf[1:3], tfr.field) + termLen := copy(buf[3:], tfr.term) + buf[3+termLen] = ByteSeparator + return buf +} + +func (tfr *TermFrequencyRow) Key() []byte { + buf := make([]byte, tfr.KeySize()) + size, _ := tfr.KeyTo(buf) + return buf[:size] +} + +func (tfr *TermFrequencyRow) KeySize() int { + return termFrequencyRowKeySize(tfr.term, tfr.doc) +} + +func termFrequencyRowKeySize(term, doc []byte) int { + return 3 + len(term) + 1 + len(doc) +} + +func (tfr *TermFrequencyRow) KeyTo(buf []byte) (int, error) { + return termFrequencyRowKeyTo(buf, tfr.field, tfr.term, tfr.doc), nil +} + +func termFrequencyRowKeyTo(buf []byte, field uint16, term, doc []byte) int { + buf[0] = 't' + binary.LittleEndian.PutUint16(buf[1:3], field) + termLen := copy(buf[3:], term) + buf[3+termLen] = ByteSeparator + docLen := copy(buf[3+termLen+1:], doc) + return 3 + termLen + 1 + docLen +} + +func (tfr *TermFrequencyRow) KeyAppendTo(buf []byte) ([]byte, error) { + keySize := tfr.KeySize() + if cap(buf) < keySize { + buf = make([]byte, keySize) + } + actualSize, err := tfr.KeyTo(buf[0:keySize]) + return buf[0:actualSize], err +} + +func (tfr *TermFrequencyRow) DictionaryRowKey() []byte { + dr := NewDictionaryRow(tfr.term, tfr.field, 0) + return dr.Key() +} + +func (tfr *TermFrequencyRow) DictionaryRowKeySize() int { + dr := NewDictionaryRow(tfr.term, tfr.field, 0) + return dr.KeySize() +} + +func (tfr *TermFrequencyRow) DictionaryRowKeyTo(buf []byte) (int, error) { + dr := NewDictionaryRow(tfr.term, tfr.field, 0) + return dr.KeyTo(buf) +} + +func (tfr *TermFrequencyRow) Value() []byte { + buf := make([]byte, tfr.ValueSize()) + size, _ := tfr.ValueTo(buf) + return buf[:size] +} + +func (tfr *TermFrequencyRow) ValueSize() int { + bufLen := binary.MaxVarintLen64 + binary.MaxVarintLen64 + for _, vector := range tfr.vectors { + bufLen += (binary.MaxVarintLen64 * 4) + (1+len(vector.arrayPositions))*binary.MaxVarintLen64 + } + return bufLen +} + +func (tfr *TermFrequencyRow) ValueTo(buf []byte) (int, error) { + used := binary.PutUvarint(buf[:binary.MaxVarintLen64], tfr.freq) + + normuint32 := math.Float32bits(tfr.norm) + newbuf := buf[used : used+binary.MaxVarintLen64] + used += binary.PutUvarint(newbuf, uint64(normuint32)) + + for _, vector := range tfr.vectors { + used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], uint64(vector.field)) + used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], vector.pos) + used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], vector.start) + used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], vector.end) + used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], uint64(len(vector.arrayPositions))) + for _, arrayPosition := range vector.arrayPositions { + used += binary.PutUvarint(buf[used:used+binary.MaxVarintLen64], arrayPosition) + } + } + return used, nil +} + +func (tfr *TermFrequencyRow) String() string { + return fmt.Sprintf("Term: `%s` Field: %d DocId: `%s` Frequency: %d Norm: %f Vectors: %v", string(tfr.term), tfr.field, string(tfr.doc), tfr.freq, tfr.norm, tfr.vectors) +} + +func InitTermFrequencyRow(tfr *TermFrequencyRow, term []byte, field uint16, docID []byte, freq uint64, norm float32) *TermFrequencyRow { + tfr.term = term + tfr.field = field + tfr.doc = docID + tfr.freq = freq + tfr.norm = norm + return tfr +} + +func NewTermFrequencyRow(term []byte, field uint16, docID []byte, freq uint64, norm float32) *TermFrequencyRow { + return &TermFrequencyRow{ + term: term, + field: field, + doc: docID, + freq: freq, + norm: norm, + } +} + +func NewTermFrequencyRowWithTermVectors(term []byte, field uint16, docID []byte, freq uint64, norm float32, vectors []*TermVector) *TermFrequencyRow { + return &TermFrequencyRow{ + term: term, + field: field, + doc: docID, + freq: freq, + norm: norm, + vectors: vectors, + } +} + +func NewTermFrequencyRowK(key []byte) (*TermFrequencyRow, error) { + rv := &TermFrequencyRow{} + err := rv.parseK(key) + if err != nil { + return nil, err + } + return rv, nil +} + +func (tfr *TermFrequencyRow) parseK(key []byte) error { + keyLen := len(key) + if keyLen < 3 { + return fmt.Errorf("invalid term frequency key, no valid field") + } + tfr.field = binary.LittleEndian.Uint16(key[1:3]) + + termEndPos := bytes.IndexByte(key[3:], ByteSeparator) + if termEndPos < 0 { + return fmt.Errorf("invalid term frequency key, no byte separator terminating term") + } + tfr.term = key[3 : 3+termEndPos] + + docLen := keyLen - (3 + termEndPos + 1) + if docLen < 1 { + return fmt.Errorf("invalid term frequency key, empty docid") + } + tfr.doc = key[3+termEndPos+1:] + + return nil +} + +func (tfr *TermFrequencyRow) parseKDoc(key []byte, term []byte) error { + tfr.doc = key[3+len(term)+1:] + if len(tfr.doc) == 0 { + return fmt.Errorf("invalid term frequency key, empty docid") + } + + return nil +} + +func (tfr *TermFrequencyRow) parseV(value []byte, includeTermVectors bool) error { + var bytesRead int + tfr.freq, bytesRead = binary.Uvarint(value) + if bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, invalid frequency") + } + currOffset := bytesRead + + var norm uint64 + norm, bytesRead = binary.Uvarint(value[currOffset:]) + if bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, no norm") + } + currOffset += bytesRead + + tfr.norm = math.Float32frombits(uint32(norm)) + + tfr.vectors = nil + if !includeTermVectors { + return nil + } + + var field uint64 + field, bytesRead = binary.Uvarint(value[currOffset:]) + for bytesRead > 0 { + currOffset += bytesRead + tv := TermVector{} + tv.field = uint16(field) + // at this point we expect at least one term vector + if tfr.vectors == nil { + tfr.vectors = make([]*TermVector, 0) + } + + tv.pos, bytesRead = binary.Uvarint(value[currOffset:]) + if bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, vector contains no position") + } + currOffset += bytesRead + + tv.start, bytesRead = binary.Uvarint(value[currOffset:]) + if bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, vector contains no start") + } + currOffset += bytesRead + + tv.end, bytesRead = binary.Uvarint(value[currOffset:]) + if bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, vector contains no end") + } + currOffset += bytesRead + + var arrayPositionsLen uint64 + arrayPositionsLen, bytesRead = binary.Uvarint(value[currOffset:]) + if bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, vector contains no arrayPositionLen") + } + currOffset += bytesRead + + if arrayPositionsLen > 0 { + tv.arrayPositions = make([]uint64, arrayPositionsLen) + for i := 0; uint64(i) < arrayPositionsLen; i++ { + tv.arrayPositions[i], bytesRead = binary.Uvarint(value[currOffset:]) + if bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, vector contains no arrayPosition of index %d", i) + } + currOffset += bytesRead + } + } + + tfr.vectors = append(tfr.vectors, &tv) + // try to read next record (may not exist) + field, bytesRead = binary.Uvarint(value[currOffset:]) + } + if len(value[currOffset:]) > 0 && bytesRead <= 0 { + return fmt.Errorf("invalid term frequency value, vector field invalid") + } + + return nil +} + +func NewTermFrequencyRowKV(key, value []byte) (*TermFrequencyRow, error) { + rv, err := NewTermFrequencyRowK(key) + if err != nil { + return nil, err + } + + err = rv.parseV(value, true) + if err != nil { + return nil, err + } + return rv, nil +} + +type BackIndexRow struct { + doc []byte + termsEntries []*BackIndexTermsEntry + storedEntries []*BackIndexStoreEntry +} + +func (br *BackIndexRow) AllTermKeys() [][]byte { + if br == nil { + return nil + } + rv := make([][]byte, 0, len(br.termsEntries)) // FIXME this underestimates severely + for _, termsEntry := range br.termsEntries { + for i := range termsEntry.Terms { + termRow := NewTermFrequencyRow([]byte(termsEntry.Terms[i]), uint16(termsEntry.GetField()), br.doc, 0, 0) + rv = append(rv, termRow.Key()) + } + } + return rv +} + +func (br *BackIndexRow) AllStoredKeys() [][]byte { + if br == nil { + return nil + } + rv := make([][]byte, len(br.storedEntries)) + for i, storedEntry := range br.storedEntries { + storedRow := NewStoredRow(br.doc, uint16(storedEntry.GetField()), storedEntry.GetArrayPositions(), 'x', []byte{}) + rv[i] = storedRow.Key() + } + return rv +} + +func (br *BackIndexRow) Key() []byte { + buf := make([]byte, br.KeySize()) + size, _ := br.KeyTo(buf) + return buf[:size] +} + +func (br *BackIndexRow) KeySize() int { + return len(br.doc) + 1 +} + +func (br *BackIndexRow) KeyTo(buf []byte) (int, error) { + buf[0] = 'b' + used := copy(buf[1:], br.doc) + return used + 1, nil +} + +func (br *BackIndexRow) Value() []byte { + buf := make([]byte, br.ValueSize()) + size, _ := br.ValueTo(buf) + return buf[:size] +} + +func (br *BackIndexRow) ValueSize() int { + birv := &BackIndexRowValue{ + TermsEntries: br.termsEntries, + StoredEntries: br.storedEntries, + } + return birv.Size() +} + +func (br *BackIndexRow) ValueTo(buf []byte) (int, error) { + birv := &BackIndexRowValue{ + TermsEntries: br.termsEntries, + StoredEntries: br.storedEntries, + } + return birv.MarshalTo(buf) +} + +func (br *BackIndexRow) String() string { + return fmt.Sprintf("Backindex DocId: `%s` Terms Entries: %v, Stored Entries: %v", string(br.doc), br.termsEntries, br.storedEntries) +} + +func NewBackIndexRow(docID []byte, entries []*BackIndexTermsEntry, storedFields []*BackIndexStoreEntry) *BackIndexRow { + return &BackIndexRow{ + doc: docID, + termsEntries: entries, + storedEntries: storedFields, + } +} + +func NewBackIndexRowKV(key, value []byte) (*BackIndexRow, error) { + rv := BackIndexRow{} + + buf := bytes.NewBuffer(key) + _, err := buf.ReadByte() // type + if err != nil { + return nil, err + } + + rv.doc, err = buf.ReadBytes(ByteSeparator) + if err == io.EOF && len(rv.doc) < 1 { + err = fmt.Errorf("invalid doc length 0 - % x", key) + } + if err != nil && err != io.EOF { + return nil, err + } else if err == nil { + rv.doc = rv.doc[:len(rv.doc)-1] // trim off separator byte + } + + var birv BackIndexRowValue + err = proto.Unmarshal(value, &birv) + if err != nil { + return nil, err + } + rv.termsEntries = birv.TermsEntries + rv.storedEntries = birv.StoredEntries + + return &rv, nil +} + +// STORED + +type StoredRow struct { + doc []byte + field uint16 + arrayPositions []uint64 + typ byte + value []byte +} + +func (s *StoredRow) Key() []byte { + buf := make([]byte, s.KeySize()) + size, _ := s.KeyTo(buf) + return buf[0:size] +} + +func (s *StoredRow) KeySize() int { + return 1 + len(s.doc) + 1 + 2 + (binary.MaxVarintLen64 * len(s.arrayPositions)) +} + +func (s *StoredRow) KeyTo(buf []byte) (int, error) { + docLen := len(s.doc) + buf[0] = 's' + copy(buf[1:], s.doc) + buf[1+docLen] = ByteSeparator + binary.LittleEndian.PutUint16(buf[1+docLen+1:], s.field) + bytesUsed := 1 + docLen + 1 + 2 + for _, arrayPosition := range s.arrayPositions { + varbytes := binary.PutUvarint(buf[bytesUsed:], arrayPosition) + bytesUsed += varbytes + } + return bytesUsed, nil +} + +func (s *StoredRow) Value() []byte { + buf := make([]byte, s.ValueSize()) + size, _ := s.ValueTo(buf) + return buf[:size] +} + +func (s *StoredRow) ValueSize() int { + return len(s.value) + 1 +} + +func (s *StoredRow) ValueTo(buf []byte) (int, error) { + buf[0] = s.typ + used := copy(buf[1:], s.value) + return used + 1, nil +} + +func (s *StoredRow) String() string { + return fmt.Sprintf("Document: %s Field %d, Array Positions: %v, Type: %s Value: %s", s.doc, s.field, s.arrayPositions, string(s.typ), s.value) +} + +func (s *StoredRow) ScanPrefixForDoc() []byte { + docLen := len(s.doc) + buf := make([]byte, 1+docLen+1) + buf[0] = 's' + copy(buf[1:], s.doc) + buf[1+docLen] = ByteSeparator + return buf +} + +func NewStoredRow(docID []byte, field uint16, arrayPositions []uint64, typ byte, value []byte) *StoredRow { + return &StoredRow{ + doc: docID, + field: field, + arrayPositions: arrayPositions, + typ: typ, + value: value, + } +} + +func NewStoredRowK(key []byte) (*StoredRow, error) { + rv := StoredRow{} + + buf := bytes.NewBuffer(key) + _, err := buf.ReadByte() // type + if err != nil { + return nil, err + } + + rv.doc, err = buf.ReadBytes(ByteSeparator) + if err != nil { + return nil, err + } + if len(rv.doc) < 2 { // 1 for min doc id length, 1 for separator + err = fmt.Errorf("invalid doc length 0") + return nil, err + } + + rv.doc = rv.doc[:len(rv.doc)-1] // trim off separator byte + + err = binary.Read(buf, binary.LittleEndian, &rv.field) + if err != nil { + return nil, err + } + + rv.arrayPositions = make([]uint64, 0) + nextArrayPos, err := binary.ReadUvarint(buf) + for err == nil { + rv.arrayPositions = append(rv.arrayPositions, nextArrayPos) + nextArrayPos, err = binary.ReadUvarint(buf) + } + return &rv, nil +} + +func NewStoredRowKV(key, value []byte) (*StoredRow, error) { + rv, err := NewStoredRowK(key) + if err != nil { + return nil, err + } + rv.typ = value[0] + rv.value = value[1:] + return rv, nil +} + +type backIndexFieldTermVisitor func(field uint32, term []byte) + +// visitBackIndexRow is designed to process a protobuf encoded +// value, without creating unnecessary garbage. Instead values are passed +// to a callback, inspected first, and only copied if necessary. +// Due to the fact that this borrows from generated code, it must be marnually +// updated if the protobuf definition changes. +// +// This code originates from: +// func (m *BackIndexRowValue) Unmarshal(data []byte) error +// the sections which create garbage or parse unintersting sections +// have been commented out. This was done by design to allow for easier +// merging in the future if that original function is regenerated +func visitBackIndexRow(data []byte, callback backIndexFieldTermVisitor) error { + l := len(data) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TermsEntries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + postIndex := iNdEx + msglen + if msglen < 0 { + return ErrInvalidLengthUpsidedown + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + // dont parse term entries + // m.TermsEntries = append(m.TermsEntries, &BackIndexTermsEntry{}) + // if err := m.TermsEntries[len(m.TermsEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil { + // return err + // } + // instead, inspect them + if err := visitBackIndexRowFieldTerms(data[iNdEx:postIndex], callback); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StoredEntries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + postIndex := iNdEx + msglen + if msglen < 0 { + return ErrInvalidLengthUpsidedown + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + // don't parse stored entries + // m.StoredEntries = append(m.StoredEntries, &BackIndexStoreEntry{}) + // if err := m.StoredEntries[len(m.StoredEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil { + // return err + // } + iNdEx = postIndex + default: + var sizeOfWire int + for { + sizeOfWire++ + wire >>= 7 + if wire == 0 { + break + } + } + iNdEx -= sizeOfWire + skippy, err := skipUpsidedown(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthUpsidedown + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + // don't track unrecognized data + // m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + return nil +} + +// visitBackIndexRowFieldTerms is designed to process a protobuf encoded +// sub-value within the BackIndexRowValue, without creating unnecessary garbage. +// Instead values are passed to a callback, inspected first, and only copied if +// necessary. Due to the fact that this borrows from generated code, it must +// be marnually updated if the protobuf definition changes. +// +// This code originates from: +// func (m *BackIndexTermsEntry) Unmarshal(data []byte) error { +// the sections which create garbage or parse uninteresting sections +// have been commented out. This was done by design to allow for easier +// merging in the future if that original function is regenerated +func visitBackIndexRowFieldTerms(data []byte, callback backIndexFieldTermVisitor) error { + var theField uint32 + + var hasFields [1]uint64 + l := len(data) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Field", wireType) + } + var v uint32 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + v |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + // m.Field = &v + theField = v + hasFields[0] |= uint64(0x00000001) + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Terms", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + postIndex := iNdEx + int(stringLen) + if postIndex > l { + return io.ErrUnexpectedEOF + } + // m.Terms = append(m.Terms, string(data[iNdEx:postIndex])) + callback(theField, data[iNdEx:postIndex]) + iNdEx = postIndex + default: + var sizeOfWire int + for { + sizeOfWire++ + wire >>= 7 + if wire == 0 { + break + } + } + iNdEx -= sizeOfWire + skippy, err := skipUpsidedown(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthUpsidedown + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + // m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + // if hasFields[0]&uint64(0x00000001) == 0 { + // return new(github_com_golang_protobuf_proto.RequiredNotSetError) + // } + + return nil +} diff --git a/index/upsidedown/row_merge.go b/index/upsidedown/row_merge.go new file mode 100644 index 0000000..39172ad --- /dev/null +++ b/index/upsidedown/row_merge.go @@ -0,0 +1,76 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "encoding/binary" +) + +var mergeOperator upsideDownMerge + +var dictionaryTermIncr []byte +var dictionaryTermDecr []byte + +func init() { + dictionaryTermIncr = make([]byte, 8) + binary.LittleEndian.PutUint64(dictionaryTermIncr, uint64(1)) + dictionaryTermDecr = make([]byte, 8) + var negOne = int64(-1) + binary.LittleEndian.PutUint64(dictionaryTermDecr, uint64(negOne)) +} + +type upsideDownMerge struct{} + +func (m *upsideDownMerge) FullMerge(key, existingValue []byte, operands [][]byte) ([]byte, bool) { + // set up record based on key + dr, err := NewDictionaryRowK(key) + if err != nil { + return nil, false + } + if len(existingValue) > 0 { + // if existing value, parse it + err = dr.parseDictionaryV(existingValue) + if err != nil { + return nil, false + } + } + + // now process operands + for _, operand := range operands { + next := int64(binary.LittleEndian.Uint64(operand)) + if next < 0 && uint64(-next) > dr.count { + // subtracting next from existing would overflow + dr.count = 0 + } else if next < 0 { + dr.count -= uint64(-next) + } else { + dr.count += uint64(next) + } + } + + return dr.Value(), true +} + +func (m *upsideDownMerge) PartialMerge(key, leftOperand, rightOperand []byte) ([]byte, bool) { + left := int64(binary.LittleEndian.Uint64(leftOperand)) + right := int64(binary.LittleEndian.Uint64(rightOperand)) + rv := make([]byte, 8) + binary.LittleEndian.PutUint64(rv, uint64(left+right)) + return rv, true +} + +func (m *upsideDownMerge) Name() string { + return "upsideDownMerge" +} diff --git a/index/upsidedown/row_merge_test.go b/index/upsidedown/row_merge_test.go new file mode 100644 index 0000000..66eb9cb --- /dev/null +++ b/index/upsidedown/row_merge_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "bytes" + "encoding/binary" + "testing" +) + +func TestPartialMerge(t *testing.T) { + + tests := []struct { + in [][]byte + out uint64 + }{ + { + in: [][]byte{dictionaryTermIncr, dictionaryTermIncr, dictionaryTermIncr, dictionaryTermIncr, dictionaryTermIncr}, + out: 5, + }, + } + + mo := &upsideDownMerge{} + for _, test := range tests { + curr := test.in[0] + for _, next := range test.in[1:] { + var ok bool + curr, ok = mo.PartialMerge([]byte("key"), curr, next) + if !ok { + t.Errorf("expected partial merge ok") + } + } + actual := decodeCount(curr) + if actual != test.out { + t.Errorf("expected %d, got %d", test.out, actual) + } + } + +} + +func decodeCount(in []byte) uint64 { + buf := bytes.NewBuffer(in) + count, _ := binary.ReadUvarint(buf) + return count +} diff --git a/index/upsidedown/row_test.go b/index/upsidedown/row_test.go new file mode 100644 index 0000000..8b07d4f --- /dev/null +++ b/index/upsidedown/row_test.go @@ -0,0 +1,382 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "math" + "reflect" + "testing" + + "github.com/golang/protobuf/proto" +) + +func TestRows(t *testing.T) { + tests := []struct { + input UpsideDownCouchRow + outKey []byte + outVal []byte + }{ + { + NewVersionRow(1), + []byte{'v'}, + []byte{0x1}, + }, + { + NewFieldRow(0, "name"), + []byte{'f', 0, 0}, + []byte{'n', 'a', 'm', 'e', ByteSeparator}, + }, + { + NewFieldRow(1, "desc"), + []byte{'f', 1, 0}, + []byte{'d', 'e', 's', 'c', ByteSeparator}, + }, + { + NewFieldRow(513, "style"), + []byte{'f', 1, 2}, + []byte{'s', 't', 'y', 'l', 'e', ByteSeparator}, + }, + { + NewDictionaryRow([]byte{'b', 'e', 'e', 'r'}, 0, 27), + []byte{'d', 0, 0, 'b', 'e', 'e', 'r'}, + []byte{27}, + }, + { + NewTermFrequencyRow([]byte{'b', 'e', 'e', 'r'}, 0, []byte("catz"), 3, 3.14), + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'c', 'a', 't', 'z'}, + []byte{3, 195, 235, 163, 130, 4}, + }, + { + NewTermFrequencyRow([]byte{'b', 'e', 'e', 'r'}, 0, []byte("budweiser"), 3, 3.14), + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{3, 195, 235, 163, 130, 4}, + }, + { + NewTermFrequencyRowWithTermVectors([]byte{'b', 'e', 'e', 'r'}, 0, []byte("budweiser"), 3, 3.14, []*TermVector{{field: 0, pos: 1, start: 3, end: 11}, {field: 0, pos: 2, start: 23, end: 31}, {field: 0, pos: 3, start: 43, end: 51}}), + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{3, 195, 235, 163, 130, 4, 0, 1, 3, 11, 0, 0, 2, 23, 31, 0, 0, 3, 43, 51, 0}, + }, + // test larger varints + { + NewTermFrequencyRowWithTermVectors([]byte{'b', 'e', 'e', 'r'}, 0, []byte("budweiser"), 25896, 3.14, []*TermVector{{field: 255, pos: 1, start: 3, end: 11}, {field: 0, pos: 2198, start: 23, end: 31}, {field: 0, pos: 3, start: 43, end: 51}}), + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{168, 202, 1, 195, 235, 163, 130, 4, 255, 1, 1, 3, 11, 0, 0, 150, 17, 23, 31, 0, 0, 3, 43, 51, 0}, + }, + // test vectors with arrayPositions + { + NewTermFrequencyRowWithTermVectors([]byte{'b', 'e', 'e', 'r'}, 0, []byte("budweiser"), 25896, 3.14, []*TermVector{{field: 255, pos: 1, start: 3, end: 11, arrayPositions: []uint64{0}}, {field: 0, pos: 2198, start: 23, end: 31, arrayPositions: []uint64{1, 2}}, {field: 0, pos: 3, start: 43, end: 51, arrayPositions: []uint64{3, 4, 5}}}), + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{168, 202, 1, 195, 235, 163, 130, 4, 255, 1, 1, 3, 11, 1, 0, 0, 150, 17, 23, 31, 2, 1, 2, 0, 3, 43, 51, 3, 3, 4, 5}, + }, + { + NewBackIndexRow([]byte("budweiser"), []*BackIndexTermsEntry{{Field: proto.Uint32(0), Terms: []string{"beer"}}}, nil), + []byte{'b', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{10, 8, 8, 0, 18, 4, 'b', 'e', 'e', 'r'}, + }, + { + NewBackIndexRow([]byte("budweiser"), []*BackIndexTermsEntry{{Field: proto.Uint32(0), Terms: []string{"beer"}}, {Field: proto.Uint32(1), Terms: []string{"beat"}}}, nil), + []byte{'b', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{10, 8, 8, 0, 18, 4, 'b', 'e', 'e', 'r', 10, 8, 8, 1, 18, 4, 'b', 'e', 'a', 't'}, + }, + { + NewBackIndexRow([]byte("budweiser"), []*BackIndexTermsEntry{{Field: proto.Uint32(0), Terms: []string{"beer"}}, {Field: proto.Uint32(1), Terms: []string{"beat"}}}, []*BackIndexStoreEntry{{Field: proto.Uint32(3)}, {Field: proto.Uint32(4)}, {Field: proto.Uint32(5)}}), + []byte{'b', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{10, 8, 8, 0, 18, 4, 'b', 'e', 'e', 'r', 10, 8, 8, 1, 18, 4, 'b', 'e', 'a', 't', 18, 2, 8, 3, 18, 2, 8, 4, 18, 2, 8, 5}, + }, + { + NewStoredRow([]byte("budweiser"), 0, []uint64{}, byte('t'), []byte("an american beer")), + []byte{'s', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r', ByteSeparator, 0, 0}, + []byte{'t', 'a', 'n', ' ', 'a', 'm', 'e', 'r', 'i', 'c', 'a', 'n', ' ', 'b', 'e', 'e', 'r'}, + }, + { + NewStoredRow([]byte("budweiser"), 0, []uint64{2, 294, 3078}, byte('t'), []byte("an american beer")), + []byte{'s', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r', ByteSeparator, 0, 0, 2, 166, 2, 134, 24}, + []byte{'t', 'a', 'n', ' ', 'a', 'm', 'e', 'r', 'i', 'c', 'a', 'n', ' ', 'b', 'e', 'e', 'r'}, + }, + { + NewInternalRow([]byte("mapping"), []byte(`{"mapping":"json content"}`)), + []byte{'i', 'm', 'a', 'p', 'p', 'i', 'n', 'g'}, + []byte{'{', '"', 'm', 'a', 'p', 'p', 'i', 'n', 'g', '"', ':', '"', 'j', 's', 'o', 'n', ' ', 'c', 'o', 'n', 't', 'e', 'n', 't', '"', '}'}, + }, + } + + // test going from struct to k/v bytes + for i, test := range tests { + rk := test.input.Key() + if !reflect.DeepEqual(rk, test.outKey) { + t.Errorf("Expected key to be %v got: %v", test.outKey, rk) + } + rv := test.input.Value() + if !reflect.DeepEqual(rv, test.outVal) { + t.Errorf("Expected value to be %v got: %v for %d", test.outVal, rv, i) + } + } + + // now test going back from k/v bytes to struct + for i, test := range tests { + row, err := ParseFromKeyValue(test.outKey, test.outVal) + if err != nil { + t.Errorf("error parsking key/value: %v", err) + } + if !reflect.DeepEqual(row, test.input) { + t.Errorf("Expected: %#v got: %#v for %d", test.input, row, i) + } + } + +} + +func TestInvalidRows(t *testing.T) { + tests := []struct { + key []byte + val []byte + }{ + // empty key + { + []byte{}, + []byte{}, + }, + // no such type q + { + []byte{'q'}, + []byte{}, + }, + // type v, invalid empty value + { + []byte{'v'}, + []byte{}, + }, + // type f, invalid key + { + []byte{'f'}, + []byte{}, + }, + // type f, valid key, invalid value + { + []byte{'f', 0, 0}, + []byte{}, + }, + // type t, invalid key (missing field) + { + []byte{'t'}, + []byte{}, + }, + // type t, invalid key (missing term) + { + []byte{'t', 0, 0}, + []byte{}, + }, + // type t, invalid key (missing id) + { + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator}, + []byte{}, + }, + // type t, invalid val (missing freq) + { + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{}, + }, + // type t, invalid val (missing norm) + { + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{3}, + }, + // type t, invalid val (half missing tv field, full missing is valid (no term vectors)) + { + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{3, 25, 255}, + }, + // type t, invalid val (missing tv pos) + { + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{3, 25, 0}, + }, + // type t, invalid val (missing tv start) + { + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{3, 25, 0, 0}, + }, + // type t, invalid val (missing tv end) + { + []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{3, 25, 0, 0, 0}, + }, + // type b, invalid key (missing id) + { + []byte{'b'}, + []byte{'b', 'e', 'e', 'r', ByteSeparator, 0, 0}, + }, + // type b, invalid val (missing field) + { + []byte{'b', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'}, + []byte{'g', 'a', 'r', 'b', 'a', 'g', 'e'}, + }, + // type s, invalid key (missing id) + { + []byte{'s'}, + []byte{'t', 'a', 'n', ' ', 'a', 'm', 'e', 'r', 'i', 'c', 'a', 'n', ' ', 'b', 'e', 'e', 'r'}, + }, + // type b, invalid val (missing field) + { + []byte{'s', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r', ByteSeparator}, + []byte{'t', 'a', 'n', ' ', 'a', 'm', 'e', 'r', 'i', 'c', 'a', 'n', ' ', 'b', 'e', 'e', 'r'}, + }, + } + + for _, test := range tests { + _, err := ParseFromKeyValue(test.key, test.val) + if err == nil { + t.Errorf("expected error, got nil") + } + } +} + +func TestDictionaryRowValueBug197(t *testing.T) { + // this was the smallest value that would trigger a crash + dr := &DictionaryRow{ + field: 0, + term: []byte("marty"), + count: 72057594037927936, + } + dr.Value() + // this is the maximum possible value + dr = &DictionaryRow{ + field: 0, + term: []byte("marty"), + count: math.MaxUint64, + } + dr.Value() + // neither of these should panic +} + +func BenchmarkTermFrequencyRowEncode(b *testing.B) { + row := NewTermFrequencyRowWithTermVectors( + []byte{'b', 'e', 'e', 'r'}, + 0, + []byte("budweiser"), + 3, + 3.14, + []*TermVector{ + { + field: 0, + pos: 1, + start: 3, + end: 11, + }, + { + field: 0, + pos: 2, + start: 23, + end: 31, + }, + { + field: 0, + pos: 3, + start: 43, + end: 51, + }, + }) + b.ResetTimer() + for i := 0; i < b.N; i++ { + row.Key() + row.Value() + } +} + +func BenchmarkTermFrequencyRowDecode(b *testing.B) { + k := []byte{'t', 0, 0, 'b', 'e', 'e', 'r', ByteSeparator, 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r'} + v := []byte{3, 195, 235, 163, 130, 4, 0, 1, 3, 11, 0, 0, 2, 23, 31, 0, 0, 3, 43, 51, 0} + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := NewTermFrequencyRowKV(k, v) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBackIndexRowEncode(b *testing.B) { + field := uint32(1) + t1 := "term1" + row := NewBackIndexRow([]byte("beername"), + []*BackIndexTermsEntry{ + { + Field: &field, + Terms: []string{t1}, + }, + }, + []*BackIndexStoreEntry{ + { + Field: &field, + }, + }) + b.ResetTimer() + for i := 0; i < b.N; i++ { + row.Key() + row.Value() + b.Logf("%#v", row.Value()) + } +} + +func BenchmarkBackIndexRowDecode(b *testing.B) { + k := []byte{0x62, 0x62, 0x65, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65} + v := []byte{0xa, 0x9, 0x8, 0x1, 0x12, 0x5, 0x74, 0x65, 0x72, 0x6d, 0x31, 0x12, 0x2, 0x8, 0x1} + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := NewBackIndexRowKV(k, v) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkStoredRowEncode(b *testing.B) { + row := NewStoredRow([]byte("budweiser"), 0, []uint64{}, byte('t'), []byte("an american beer")) + b.ResetTimer() + for i := 0; i < b.N; i++ { + row.Key() + row.Value() + } +} + +func BenchmarkStoredRowDecode(b *testing.B) { + k := []byte{'s', 'b', 'u', 'd', 'w', 'e', 'i', 's', 'e', 'r', ByteSeparator, 0, 0} + v := []byte{'t', 'a', 'n', ' ', 'a', 'm', 'e', 'r', 'i', 'c', 'a', 'n', ' ', 'b', 'e', 'e', 'r'} + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := NewStoredRowKV(k, v) + if err != nil { + b.Fatal(err) + } + } +} + +func TestVisitBackIndexRow(t *testing.T) { + expected := map[uint32][]byte{ + 0: []byte("beer"), + 1: []byte("beat"), + } + val := []byte{10, 8, 8, 0, 18, 4, 'b', 'e', 'e', 'r', 10, 8, 8, 1, 18, 4, 'b', 'e', 'a', 't', 18, 2, 8, 3, 18, 2, 8, 4, 18, 2, 8, 5} + err := visitBackIndexRow(val, func(field uint32, term []byte) { + if reflect.DeepEqual(expected[field], term) { + delete(expected, field) + } + }) + if err != nil { + t.Fatal(err) + } + if len(expected) > 0 { + t.Errorf("expected visitor to see these but did not %v", expected) + } +} diff --git a/index/upsidedown/stats.go b/index/upsidedown/stats.go new file mode 100644 index 0000000..5fea7c1 --- /dev/null +++ b/index/upsidedown/stats.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "sync/atomic" + + "github.com/blevesearch/bleve/v2/util" + "github.com/blevesearch/upsidedown_store_api" +) + +type indexStat struct { + updates, deletes, batches, errors uint64 + analysisTime, indexTime uint64 + termSearchersStarted uint64 + termSearchersFinished uint64 + numPlainTextBytesIndexed uint64 + i *UpsideDownCouch +} + +func (i *indexStat) statsMap() map[string]interface{} { + m := map[string]interface{}{} + m["updates"] = atomic.LoadUint64(&i.updates) + m["deletes"] = atomic.LoadUint64(&i.deletes) + m["batches"] = atomic.LoadUint64(&i.batches) + m["errors"] = atomic.LoadUint64(&i.errors) + m["analysis_time"] = atomic.LoadUint64(&i.analysisTime) + m["index_time"] = atomic.LoadUint64(&i.indexTime) + m["term_searchers_started"] = atomic.LoadUint64(&i.termSearchersStarted) + m["term_searchers_finished"] = atomic.LoadUint64(&i.termSearchersFinished) + m["num_plain_text_bytes_indexed"] = atomic.LoadUint64(&i.numPlainTextBytesIndexed) + + if o, ok := i.i.store.(store.KVStoreStats); ok { + m["kv"] = o.StatsMap() + } + + return m +} + +func (i *indexStat) MarshalJSON() ([]byte, error) { + m := i.statsMap() + return util.MarshalJSON(m) +} diff --git a/index/upsidedown/store/boltdb/iterator.go b/index/upsidedown/store/boltdb/iterator.go new file mode 100644 index 0000000..cf4da87 --- /dev/null +++ b/index/upsidedown/store/boltdb/iterator.go @@ -0,0 +1,85 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "bytes" + + bolt "go.etcd.io/bbolt" +) + +type Iterator struct { + store *Store + tx *bolt.Tx + cursor *bolt.Cursor + prefix []byte + start []byte + end []byte + valid bool + key []byte + val []byte +} + +func (i *Iterator) updateValid() { + i.valid = (i.key != nil) + if i.valid { + if i.prefix != nil { + i.valid = bytes.HasPrefix(i.key, i.prefix) + } else if i.end != nil { + i.valid = bytes.Compare(i.key, i.end) < 0 + } + } +} + +func (i *Iterator) Seek(k []byte) { + if i.start != nil && bytes.Compare(k, i.start) < 0 { + k = i.start + } + if i.prefix != nil && !bytes.HasPrefix(k, i.prefix) { + if bytes.Compare(k, i.prefix) < 0 { + k = i.prefix + } else { + i.valid = false + return + } + } + i.key, i.val = i.cursor.Seek(k) + i.updateValid() +} + +func (i *Iterator) Next() { + i.key, i.val = i.cursor.Next() + i.updateValid() +} + +func (i *Iterator) Current() ([]byte, []byte, bool) { + return i.key, i.val, i.valid +} + +func (i *Iterator) Key() []byte { + return i.key +} + +func (i *Iterator) Value() []byte { + return i.val +} + +func (i *Iterator) Valid() bool { + return i.valid +} + +func (i *Iterator) Close() error { + return nil +} diff --git a/index/upsidedown/store/boltdb/reader.go b/index/upsidedown/store/boltdb/reader.go new file mode 100644 index 0000000..79513f6 --- /dev/null +++ b/index/upsidedown/store/boltdb/reader.go @@ -0,0 +1,73 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + store "github.com/blevesearch/upsidedown_store_api" + bolt "go.etcd.io/bbolt" +) + +type Reader struct { + store *Store + tx *bolt.Tx + bucket *bolt.Bucket +} + +func (r *Reader) Get(key []byte) ([]byte, error) { + var rv []byte + v := r.bucket.Get(key) + if v != nil { + rv = make([]byte, len(v)) + copy(rv, v) + } + return rv, nil +} + +func (r *Reader) MultiGet(keys [][]byte) ([][]byte, error) { + return store.MultiGet(r, keys) +} + +func (r *Reader) PrefixIterator(prefix []byte) store.KVIterator { + cursor := r.bucket.Cursor() + + rv := &Iterator{ + store: r.store, + tx: r.tx, + cursor: cursor, + prefix: prefix, + } + + rv.Seek(prefix) + return rv +} + +func (r *Reader) RangeIterator(start, end []byte) store.KVIterator { + cursor := r.bucket.Cursor() + + rv := &Iterator{ + store: r.store, + tx: r.tx, + cursor: cursor, + start: start, + end: end, + } + + rv.Seek(start) + return rv +} + +func (r *Reader) Close() error { + return r.tx.Rollback() +} diff --git a/index/upsidedown/store/boltdb/stats.go b/index/upsidedown/store/boltdb/stats.go new file mode 100644 index 0000000..d896183 --- /dev/null +++ b/index/upsidedown/store/boltdb/stats.go @@ -0,0 +1,28 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "github.com/blevesearch/bleve/v2/util" +) + +type stats struct { + s *Store +} + +func (s *stats) MarshalJSON() ([]byte, error) { + bs := s.s.db.Stats() + return util.MarshalJSON(bs) +} diff --git a/index/upsidedown/store/boltdb/store.go b/index/upsidedown/store/boltdb/store.go new file mode 100644 index 0000000..2ebe9d2 --- /dev/null +++ b/index/upsidedown/store/boltdb/store.go @@ -0,0 +1,184 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package boltdb implements a store.KVStore on top of BoltDB. It supports the +// following options: +// +// "bucket" (string): the name of BoltDB bucket to use, defaults to "bleve". +// +// "nosync" (bool): if true, set boltdb.DB.NoSync to true. It speeds up index +// operations in exchange of losing integrity guarantees if indexation aborts +// without closing the index. Use it when rebuilding indexes from zero. +package boltdb + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + + "github.com/blevesearch/bleve/v2/registry" + store "github.com/blevesearch/upsidedown_store_api" + bolt "go.etcd.io/bbolt" +) + +const ( + Name = "boltdb" + defaultCompactBatchSize = 100 +) + +type Store struct { + path string + bucket string + db *bolt.DB + noSync bool + fillPercent float64 + mo store.MergeOperator +} + +func New(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) { + path, ok := config["path"].(string) + if !ok { + return nil, fmt.Errorf("must specify path") + } + if path == "" { + return nil, os.ErrInvalid + } + + bucket, ok := config["bucket"].(string) + if !ok { + bucket = "bleve" + } + + noSync, _ := config["nosync"].(bool) + + fillPercent, ok := config["fillPercent"].(float64) + if !ok { + fillPercent = bolt.DefaultFillPercent + } + + bo := &bolt.Options{} + ro, ok := config["read_only"].(bool) + if ok { + bo.ReadOnly = ro + } + + if initialMmapSize, ok := config["initialMmapSize"].(int); ok { + bo.InitialMmapSize = initialMmapSize + } else if initialMmapSize, ok := config["initialMmapSize"].(float64); ok { + bo.InitialMmapSize = int(initialMmapSize) + } + + db, err := bolt.Open(path, 0600, bo) + if err != nil { + return nil, err + } + db.NoSync = noSync + + if !bo.ReadOnly { + err = db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(bucket)) + + return err + }) + if err != nil { + return nil, err + } + } + + rv := Store{ + path: path, + bucket: bucket, + db: db, + mo: mo, + noSync: noSync, + fillPercent: fillPercent, + } + return &rv, nil +} + +func (bs *Store) Close() error { + return bs.db.Close() +} + +func (bs *Store) Reader() (store.KVReader, error) { + tx, err := bs.db.Begin(false) + if err != nil { + return nil, err + } + return &Reader{ + store: bs, + tx: tx, + bucket: tx.Bucket([]byte(bs.bucket)), + }, nil +} + +func (bs *Store) Writer() (store.KVWriter, error) { + return &Writer{ + store: bs, + }, nil +} + +func (bs *Store) Stats() json.Marshaler { + return &stats{ + s: bs, + } +} + +// CompactWithBatchSize removes DictionaryTerm entries with a count of zero (in batchSize batches) +// Removing entries is a workaround for github issue #374. +func (bs *Store) CompactWithBatchSize(batchSize int) error { + for { + cnt := 0 + err := bs.db.Batch(func(tx *bolt.Tx) error { + c := tx.Bucket([]byte(bs.bucket)).Cursor() + prefix := []byte("d") + + for k, v := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, v = c.Next() { + if bytes.Equal(v, []byte{0}) { + cnt++ + if err := c.Delete(); err != nil { + return err + } + if cnt == batchSize { + break + } + } + + } + return nil + }) + if err != nil { + return err + } + + if cnt == 0 { + break + } + } + return nil +} + +// Compact calls CompactWithBatchSize with a default batch size of 100. This is a workaround +// for github issue #374. +func (bs *Store) Compact() error { + return bs.CompactWithBatchSize(defaultCompactBatchSize) +} + +func init() { + err := registry.RegisterKVStore(Name, New) + if err != nil { + panic(err) + } +} diff --git a/index/upsidedown/store/boltdb/store_test.go b/index/upsidedown/store/boltdb/store_test.go new file mode 100644 index 0000000..6011701 --- /dev/null +++ b/index/upsidedown/store/boltdb/store_test.go @@ -0,0 +1,148 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !darwin || !arm64 + +package boltdb + +import ( + "os" + "testing" + + store "github.com/blevesearch/upsidedown_store_api" + "github.com/blevesearch/upsidedown_store_api/test" + bolt "go.etcd.io/bbolt" +) + +func open(t *testing.T, mo store.MergeOperator) store.KVStore { + rv, err := New(mo, map[string]interface{}{"path": "test"}) + if err != nil { + t.Fatal(err) + } + return rv +} + +func cleanup(t *testing.T, s store.KVStore) { + err := s.Close() + if err != nil { + t.Fatal(err) + } + err = os.RemoveAll("test") + if err != nil { + t.Fatal(err) + } +} + +func TestBoltDBKVCrud(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestKVCrud(t, s) +} + +func TestBoltDBReaderIsolation(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderIsolation(t, s) +} + +func TestBoltDBReaderOwnsGetBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderOwnsGetBytes(t, s) +} + +func TestBoltDBWriterOwnsBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestWriterOwnsBytes(t, s) +} + +func TestBoltDBPrefixIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIterator(t, s) +} + +func TestBoltDBPrefixIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIteratorSeek(t, s) +} + +func TestBoltDBRangeIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIterator(t, s) +} + +func TestBoltDBRangeIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIteratorSeek(t, s) +} + +func TestBoltDBMerge(t *testing.T) { + s := open(t, &test.TestMergeCounter{}) + defer cleanup(t, s) + test.CommonTestMerge(t, s) +} + +func TestBoltDBConfig(t *testing.T) { + var tests = []struct { + in map[string]interface{} + path string + bucket string + noSync bool + fillPercent float64 + }{ + { + map[string]interface{}{"path": "test", "bucket": "mybucket", "nosync": true, "fillPercent": 0.75}, + "test", + "mybucket", + true, + 0.75, + }, + { + map[string]interface{}{"path": "test"}, + "test", + "bleve", + false, + bolt.DefaultFillPercent, + }, + } + + for _, test := range tests { + kv, err := New(nil, test.in) + if err != nil { + t.Fatal(err) + } + bs, ok := kv.(*Store) + if !ok { + t.Fatal("failed type assertion to *boltdb.Store") + } + if bs.path != test.path { + t.Fatalf("path: expected %q, got %q", test.path, bs.path) + } + if bs.bucket != test.bucket { + t.Fatalf("bucket: expected %q, got %q", test.bucket, bs.bucket) + } + if bs.noSync != test.noSync { + t.Fatalf("noSync: expected %t, got %t", test.noSync, bs.noSync) + } + if bs.fillPercent != test.fillPercent { + t.Fatalf("fillPercent: expected %f, got %f", test.fillPercent, bs.fillPercent) + } + cleanup(t, kv) + } +} diff --git a/index/upsidedown/store/boltdb/writer.go b/index/upsidedown/store/boltdb/writer.go new file mode 100644 index 0000000..c25583c --- /dev/null +++ b/index/upsidedown/store/boltdb/writer.go @@ -0,0 +1,95 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "fmt" + + store "github.com/blevesearch/upsidedown_store_api" +) + +type Writer struct { + store *Store +} + +func (w *Writer) NewBatch() store.KVBatch { + return store.NewEmulatedBatch(w.store.mo) +} + +func (w *Writer) NewBatchEx(options store.KVBatchOptions) ([]byte, store.KVBatch, error) { + return make([]byte, options.TotalBytes), w.NewBatch(), nil +} + +func (w *Writer) ExecuteBatch(batch store.KVBatch) (err error) { + + emulatedBatch, ok := batch.(*store.EmulatedBatch) + if !ok { + return fmt.Errorf("wrong type of batch") + } + + tx, err := w.store.db.Begin(true) + if err != nil { + return + } + // defer function to ensure that once started, + // we either Commit tx or Rollback + defer func() { + // if nothing went wrong, commit + if err == nil { + // careful to catch error here too + err = tx.Commit() + } else { + // caller should see error that caused abort, + // not success or failure of Rollback itself + _ = tx.Rollback() + } + }() + + bucket := tx.Bucket([]byte(w.store.bucket)) + bucket.FillPercent = w.store.fillPercent + + for k, mergeOps := range emulatedBatch.Merger.Merges { + kb := []byte(k) + existingVal := bucket.Get(kb) + mergedVal, fullMergeOk := w.store.mo.FullMerge(kb, existingVal, mergeOps) + if !fullMergeOk { + err = fmt.Errorf("merge operator returned failure") + return + } + err = bucket.Put(kb, mergedVal) + if err != nil { + return + } + } + + for _, op := range emulatedBatch.Ops { + if op.V != nil { + err = bucket.Put(op.K, op.V) + if err != nil { + return + } + } else { + err = bucket.Delete(op.K) + if err != nil { + return + } + } + } + return +} + +func (w *Writer) Close() error { + return nil +} diff --git a/index/upsidedown/store/goleveldb/batch.go b/index/upsidedown/store/goleveldb/batch.go new file mode 100644 index 0000000..1536588 --- /dev/null +++ b/index/upsidedown/store/goleveldb/batch.go @@ -0,0 +1,50 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goleveldb + +import ( + "github.com/blevesearch/goleveldb/leveldb" + store "github.com/blevesearch/upsidedown_store_api" +) + +type Batch struct { + store *Store + merge *store.EmulatedMerge + batch *leveldb.Batch +} + +func (b *Batch) Set(key, val []byte) { + b.batch.Put(key, val) +} + +func (b *Batch) Delete(key []byte) { + b.batch.Delete(key) +} + +func (b *Batch) Merge(key, val []byte) { + b.merge.Merge(key, val) +} + +func (b *Batch) Reset() { + b.batch.Reset() + b.merge = store.NewEmulatedMerge(b.store.mo) +} + +func (b *Batch) Close() error { + b.batch.Reset() + b.batch = nil + b.merge = nil + return nil +} diff --git a/index/upsidedown/store/goleveldb/config.go b/index/upsidedown/store/goleveldb/config.go new file mode 100644 index 0000000..376db83 --- /dev/null +++ b/index/upsidedown/store/goleveldb/config.go @@ -0,0 +1,66 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goleveldb + +import ( + "github.com/blevesearch/goleveldb/leveldb/filter" + "github.com/blevesearch/goleveldb/leveldb/opt" +) + +func applyConfig(o *opt.Options, config map[string]interface{}) (*opt.Options, error) { + + ro, ok := config["read_only"].(bool) + if ok { + o.ReadOnly = ro + } + + cim, ok := config["create_if_missing"].(bool) + if ok { + o.ErrorIfMissing = !cim + } + + eie, ok := config["error_if_exists"].(bool) + if ok { + o.ErrorIfExist = eie + } + + wbs, ok := config["write_buffer_size"].(float64) + if ok { + o.WriteBuffer = int(wbs) + } + + bs, ok := config["block_size"].(float64) + if ok { + o.BlockSize = int(bs) + } + + bri, ok := config["block_restart_interval"].(float64) + if ok { + o.BlockRestartInterval = int(bri) + } + + lcc, ok := config["lru_cache_capacity"].(float64) + if ok { + o.BlockCacheCapacity = int(lcc) + } + + bfbpk, ok := config["bloom_filter_bits_per_key"].(float64) + if ok { + bf := filter.NewBloomFilter(int(bfbpk)) + o.Filter = bf + } + + return o, nil +} diff --git a/index/upsidedown/store/goleveldb/iterator.go b/index/upsidedown/store/goleveldb/iterator.go new file mode 100644 index 0000000..e8e002d --- /dev/null +++ b/index/upsidedown/store/goleveldb/iterator.go @@ -0,0 +1,54 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goleveldb + +import "github.com/blevesearch/goleveldb/leveldb/iterator" + +type Iterator struct { + store *Store + iterator iterator.Iterator +} + +func (ldi *Iterator) Seek(key []byte) { + ldi.iterator.Seek(key) +} + +func (ldi *Iterator) Next() { + ldi.iterator.Next() +} + +func (ldi *Iterator) Current() ([]byte, []byte, bool) { + if ldi.Valid() { + return ldi.Key(), ldi.Value(), true + } + return nil, nil, false +} + +func (ldi *Iterator) Key() []byte { + return ldi.iterator.Key() +} + +func (ldi *Iterator) Value() []byte { + return ldi.iterator.Value() +} + +func (ldi *Iterator) Valid() bool { + return ldi.iterator.Valid() +} + +func (ldi *Iterator) Close() error { + ldi.iterator.Release() + return nil +} diff --git a/index/upsidedown/store/goleveldb/reader.go b/index/upsidedown/store/goleveldb/reader.go new file mode 100644 index 0000000..9fa3a0e --- /dev/null +++ b/index/upsidedown/store/goleveldb/reader.go @@ -0,0 +1,68 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goleveldb + +import ( + "github.com/blevesearch/goleveldb/leveldb" + "github.com/blevesearch/goleveldb/leveldb/util" + store "github.com/blevesearch/upsidedown_store_api" +) + +type Reader struct { + store *Store + snapshot *leveldb.Snapshot +} + +func (r *Reader) Get(key []byte) ([]byte, error) { + b, err := r.snapshot.Get(key, r.store.defaultReadOptions) + if err == leveldb.ErrNotFound { + return nil, nil + } + return b, err +} + +func (r *Reader) MultiGet(keys [][]byte) ([][]byte, error) { + return store.MultiGet(r, keys) +} + +func (r *Reader) PrefixIterator(prefix []byte) store.KVIterator { + byteRange := util.BytesPrefix(prefix) + iter := r.snapshot.NewIterator(byteRange, r.store.defaultReadOptions) + iter.First() + rv := Iterator{ + store: r.store, + iterator: iter, + } + return &rv +} + +func (r *Reader) RangeIterator(start, end []byte) store.KVIterator { + byteRange := &util.Range{ + Start: start, + Limit: end, + } + iter := r.snapshot.NewIterator(byteRange, r.store.defaultReadOptions) + iter.First() + rv := Iterator{ + store: r.store, + iterator: iter, + } + return &rv +} + +func (r *Reader) Close() error { + r.snapshot.Release() + return nil +} diff --git a/index/upsidedown/store/goleveldb/store.go b/index/upsidedown/store/goleveldb/store.go new file mode 100644 index 0000000..3607c6d --- /dev/null +++ b/index/upsidedown/store/goleveldb/store.go @@ -0,0 +1,152 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goleveldb + +import ( + "bytes" + "fmt" + "os" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/goleveldb/leveldb" + "github.com/blevesearch/goleveldb/leveldb/opt" + "github.com/blevesearch/goleveldb/leveldb/util" + store "github.com/blevesearch/upsidedown_store_api" +) + +const ( + Name = "goleveldb" + defaultCompactBatchSize = 250 +) + +type Store struct { + path string + opts *opt.Options + db *leveldb.DB + mo store.MergeOperator + + defaultWriteOptions *opt.WriteOptions + defaultReadOptions *opt.ReadOptions +} + +func New(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) { + + path, ok := config["path"].(string) + if !ok { + return nil, fmt.Errorf("must specify path") + } + if path == "" { + return nil, os.ErrInvalid + } + + opts, err := applyConfig(&opt.Options{}, config) + if err != nil { + return nil, err + } + + db, err := leveldb.OpenFile(path, opts) + if err != nil { + return nil, err + } + + rv := Store{ + path: path, + opts: opts, + db: db, + mo: mo, + defaultReadOptions: &opt.ReadOptions{}, + defaultWriteOptions: &opt.WriteOptions{}, + } + rv.defaultWriteOptions.Sync = true + return &rv, nil +} + +func (ldbs *Store) Close() error { + return ldbs.db.Close() +} + +func (ldbs *Store) Reader() (store.KVReader, error) { + snapshot, _ := ldbs.db.GetSnapshot() + return &Reader{ + store: ldbs, + snapshot: snapshot, + }, nil +} + +func (ldbs *Store) Writer() (store.KVWriter, error) { + return &Writer{ + store: ldbs, + }, nil +} + +// CompactWithBatchSize removes DictionaryTerm entries with a count of zero (in batchSize batches), then +// compacts the underlying goleveldb store. Removing entries is a workaround for github issue #374. +func (ldbs *Store) CompactWithBatchSize(batchSize int) error { + // workaround for github issue #374 - remove DictionaryTerm keys with count=0 + batch := &leveldb.Batch{} + for { + t, err := ldbs.db.OpenTransaction() + if err != nil { + return err + } + iter := t.NewIterator(util.BytesPrefix([]byte("d")), ldbs.defaultReadOptions) + + for iter.Next() { + if bytes.Equal(iter.Value(), []byte{0}) { + k := append([]byte{}, iter.Key()...) + batch.Delete(k) + } + if batch.Len() == batchSize { + break + } + } + iter.Release() + if iter.Error() != nil { + t.Discard() + return iter.Error() + } + + if batch.Len() > 0 { + err := t.Write(batch, ldbs.defaultWriteOptions) + if err != nil { + t.Discard() + return err + } + err = t.Commit() + if err != nil { + return err + } + } else { + t.Discard() + break + } + batch.Reset() + } + + return ldbs.db.CompactRange(util.Range{Start: nil, Limit: nil}) +} + +// Compact compacts the underlying goleveldb store. The current implementation includes a workaround +// for github issue #374 (see CompactWithBatchSize). +func (ldbs *Store) Compact() error { + return ldbs.CompactWithBatchSize(defaultCompactBatchSize) +} + +func init() { + err := registry.RegisterKVStore(Name, New) + if err != nil { + panic(err) + } +} diff --git a/index/upsidedown/store/goleveldb/store_test.go b/index/upsidedown/store/goleveldb/store_test.go new file mode 100644 index 0000000..c9e5d4d --- /dev/null +++ b/index/upsidedown/store/goleveldb/store_test.go @@ -0,0 +1,99 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goleveldb + +import ( + "os" + "testing" + + store "github.com/blevesearch/upsidedown_store_api" + "github.com/blevesearch/upsidedown_store_api/test" +) + +func open(t *testing.T, mo store.MergeOperator) store.KVStore { + rv, err := New(mo, map[string]interface{}{ + "path": "test", + "create_if_missing": true, + }) + if err != nil { + t.Fatal(err) + } + return rv +} + +func cleanup(t *testing.T, s store.KVStore) { + err := s.Close() + if err != nil { + t.Fatal(err) + } + err = os.RemoveAll("test") + if err != nil { + t.Fatal(err) + } +} + +func TestGoLevelDBKVCrud(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestKVCrud(t, s) +} + +func TestGoLevelDBReaderIsolation(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderIsolation(t, s) +} + +func TestGoLevelDBReaderOwnsGetBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderOwnsGetBytes(t, s) +} + +func TestGoLevelDBWriterOwnsBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestWriterOwnsBytes(t, s) +} + +func TestGoLevelDBPrefixIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIterator(t, s) +} + +func TestGoLevelDBPrefixIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIteratorSeek(t, s) +} + +func TestGoLevelDBRangeIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIterator(t, s) +} + +func TestGoLevelDBRangeIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIteratorSeek(t, s) +} + +func TestGoLevelDBMerge(t *testing.T) { + s := open(t, &test.TestMergeCounter{}) + defer cleanup(t, s) + test.CommonTestMerge(t, s) +} diff --git a/index/upsidedown/store/goleveldb/writer.go b/index/upsidedown/store/goleveldb/writer.go new file mode 100644 index 0000000..8dfd30c --- /dev/null +++ b/index/upsidedown/store/goleveldb/writer.go @@ -0,0 +1,68 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goleveldb + +import ( + "fmt" + + "github.com/blevesearch/goleveldb/leveldb" + store "github.com/blevesearch/upsidedown_store_api" +) + +type Writer struct { + store *Store +} + +func (w *Writer) NewBatch() store.KVBatch { + rv := Batch{ + store: w.store, + merge: store.NewEmulatedMerge(w.store.mo), + batch: new(leveldb.Batch), + } + return &rv +} + +func (w *Writer) NewBatchEx(options store.KVBatchOptions) ([]byte, store.KVBatch, error) { + return make([]byte, options.TotalBytes), w.NewBatch(), nil +} + +func (w *Writer) ExecuteBatch(b store.KVBatch) error { + batch, ok := b.(*Batch) + if !ok { + return fmt.Errorf("wrong type of batch") + } + + // first process merges + for k, mergeOps := range batch.merge.Merges { + kb := []byte(k) + existingVal, err := w.store.db.Get(kb, w.store.defaultReadOptions) + if err != nil && err != leveldb.ErrNotFound { + return err + } + mergedVal, fullMergeOk := w.store.mo.FullMerge(kb, existingVal, mergeOps) + if !fullMergeOk { + return fmt.Errorf("merge operator returned failure") + } + // add the final merge to this batch + batch.batch.Put(kb, mergedVal) + } + + // now execute the batch + return w.store.db.Write(batch.batch, w.store.defaultWriteOptions) +} + +func (w *Writer) Close() error { + return nil +} diff --git a/index/upsidedown/store/gtreap/iterator.go b/index/upsidedown/store/gtreap/iterator.go new file mode 100644 index 0000000..c03f75c --- /dev/null +++ b/index/upsidedown/store/gtreap/iterator.go @@ -0,0 +1,152 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gtreap provides an in-memory implementation of the +// KVStore interfaces using the gtreap balanced-binary treap, +// copy-on-write data structure. +package gtreap + +import ( + "bytes" + "sync" + + "github.com/blevesearch/gtreap" +) + +type Iterator struct { + t *gtreap.Treap + + m sync.Mutex + cancelCh chan struct{} + nextCh chan *Item + curr *Item + currOk bool + + prefix []byte + start []byte + end []byte +} + +func (w *Iterator) Seek(k []byte) { + if w.start != nil && bytes.Compare(k, w.start) < 0 { + k = w.start + } + if w.prefix != nil && !bytes.HasPrefix(k, w.prefix) { + if bytes.Compare(k, w.prefix) < 0 { + k = w.prefix + } else { + var end []byte + for i := len(w.prefix) - 1; i >= 0; i-- { + c := w.prefix[i] + if c < 0xff { + end = make([]byte, i+1) + copy(end, w.prefix) + end[i] = c + 1 + break + } + } + k = end + } + } + w.restart(&Item{k: k}) +} + +func (w *Iterator) restart(start *Item) *Iterator { + cancelCh := make(chan struct{}) + nextCh := make(chan *Item, 1) + + w.m.Lock() + if w.cancelCh != nil { + close(w.cancelCh) + } + w.cancelCh = cancelCh + w.nextCh = nextCh + w.curr = nil + w.currOk = false + w.m.Unlock() + + go func() { + if start != nil { + w.t.VisitAscend(start, func(itm gtreap.Item) bool { + select { + case <-cancelCh: + return false + case nextCh <- itm.(*Item): + return true + } + }) + } + close(nextCh) + }() + + w.Next() + + return w +} + +func (w *Iterator) Next() { + w.m.Lock() + nextCh := w.nextCh + w.m.Unlock() + w.curr, w.currOk = <-nextCh +} + +func (w *Iterator) Current() ([]byte, []byte, bool) { + w.m.Lock() + defer w.m.Unlock() + if !w.currOk || w.curr == nil { + return nil, nil, false + } + if w.prefix != nil && !bytes.HasPrefix(w.curr.k, w.prefix) { + return nil, nil, false + } else if w.end != nil && bytes.Compare(w.curr.k, w.end) >= 0 { + return nil, nil, false + } + return w.curr.k, w.curr.v, w.currOk +} + +func (w *Iterator) Key() []byte { + k, _, ok := w.Current() + if !ok { + return nil + } + return k +} + +func (w *Iterator) Value() []byte { + _, v, ok := w.Current() + if !ok { + return nil + } + return v +} + +func (w *Iterator) Valid() bool { + _, _, ok := w.Current() + return ok +} + +func (w *Iterator) Close() error { + w.m.Lock() + if w.cancelCh != nil { + close(w.cancelCh) + } + w.cancelCh = nil + w.nextCh = nil + w.curr = nil + w.currOk = false + w.m.Unlock() + + return nil +} diff --git a/index/upsidedown/store/gtreap/reader.go b/index/upsidedown/store/gtreap/reader.go new file mode 100644 index 0000000..52b05d7 --- /dev/null +++ b/index/upsidedown/store/gtreap/reader.go @@ -0,0 +1,66 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gtreap provides an in-memory implementation of the +// KVStore interfaces using the gtreap balanced-binary treap, +// copy-on-write data structure. +package gtreap + +import ( + "github.com/blevesearch/upsidedown_store_api" + + "github.com/blevesearch/gtreap" +) + +type Reader struct { + t *gtreap.Treap +} + +func (w *Reader) Get(k []byte) (v []byte, err error) { + var rv []byte + itm := w.t.Get(&Item{k: k}) + if itm != nil { + rv = make([]byte, len(itm.(*Item).v)) + copy(rv, itm.(*Item).v) + return rv, nil + } + return nil, nil +} + +func (r *Reader) MultiGet(keys [][]byte) ([][]byte, error) { + return store.MultiGet(r, keys) +} + +func (w *Reader) PrefixIterator(k []byte) store.KVIterator { + rv := Iterator{ + t: w.t, + prefix: k, + } + rv.restart(&Item{k: k}) + return &rv +} + +func (w *Reader) RangeIterator(start, end []byte) store.KVIterator { + rv := Iterator{ + t: w.t, + start: start, + end: end, + } + rv.restart(&Item{k: start}) + return &rv +} + +func (w *Reader) Close() error { + return nil +} diff --git a/index/upsidedown/store/gtreap/store.go b/index/upsidedown/store/gtreap/store.go new file mode 100644 index 0000000..8050e4d --- /dev/null +++ b/index/upsidedown/store/gtreap/store.go @@ -0,0 +1,85 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gtreap provides an in-memory implementation of the +// KVStore interfaces using the gtreap balanced-binary treap, +// copy-on-write data structure. + +package gtreap + +import ( + "bytes" + "fmt" + "os" + "sync" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/gtreap" + store "github.com/blevesearch/upsidedown_store_api" +) + +const Name = "gtreap" + +type Store struct { + m sync.Mutex + t *gtreap.Treap + mo store.MergeOperator +} + +type Item struct { + k []byte + v []byte +} + +func itemCompare(a, b interface{}) int { + return bytes.Compare(a.(*Item).k, b.(*Item).k) +} + +func New(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) { + path, ok := config["path"].(string) + if !ok { + return nil, fmt.Errorf("must specify path") + } + if path != "" { + return nil, os.ErrInvalid + } + + rv := Store{ + t: gtreap.NewTreap(itemCompare), + mo: mo, + } + return &rv, nil +} + +func (s *Store) Close() error { + return nil +} + +func (s *Store) Reader() (store.KVReader, error) { + s.m.Lock() + t := s.t + s.m.Unlock() + return &Reader{t: t}, nil +} + +func (s *Store) Writer() (store.KVWriter, error) { + return &Writer{s: s}, nil +} + +func init() { + err := registry.RegisterKVStore(Name, New) + if err != nil { + panic(err) + } +} diff --git a/index/upsidedown/store/gtreap/store_test.go b/index/upsidedown/store/gtreap/store_test.go new file mode 100644 index 0000000..2fc3aa8 --- /dev/null +++ b/index/upsidedown/store/gtreap/store_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gtreap + +import ( + "testing" + + store "github.com/blevesearch/upsidedown_store_api" + "github.com/blevesearch/upsidedown_store_api/test" +) + +func open(t *testing.T, mo store.MergeOperator) store.KVStore { + rv, err := New(mo, map[string]interface{}{ + "path": "", + }) + if err != nil { + t.Fatal(err) + } + return rv +} + +func cleanup(t *testing.T, s store.KVStore) { + err := s.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestGTreapKVCrud(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestKVCrud(t, s) +} + +func TestGTreapReaderIsolation(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderIsolation(t, s) +} + +func TestGTreapReaderOwnsGetBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderOwnsGetBytes(t, s) +} + +func TestGTreapWriterOwnsBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestWriterOwnsBytes(t, s) +} + +func TestGTreapPrefixIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIterator(t, s) +} + +func TestGTreapPrefixIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIteratorSeek(t, s) +} + +func TestGTreapRangeIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIterator(t, s) +} + +func TestGTreapRangeIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIteratorSeek(t, s) +} + +func TestGTreapMerge(t *testing.T) { + s := open(t, &test.TestMergeCounter{}) + defer cleanup(t, s) + test.CommonTestMerge(t, s) +} diff --git a/index/upsidedown/store/gtreap/writer.go b/index/upsidedown/store/gtreap/writer.go new file mode 100644 index 0000000..80aa15b --- /dev/null +++ b/index/upsidedown/store/gtreap/writer.go @@ -0,0 +1,76 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gtreap provides an in-memory implementation of the +// KVStore interfaces using the gtreap balanced-binary treap, +// copy-on-write data structure. +package gtreap + +import ( + "fmt" + "math/rand" + + "github.com/blevesearch/upsidedown_store_api" +) + +type Writer struct { + s *Store +} + +func (w *Writer) NewBatch() store.KVBatch { + return store.NewEmulatedBatch(w.s.mo) +} + +func (w *Writer) NewBatchEx(options store.KVBatchOptions) ([]byte, store.KVBatch, error) { + return make([]byte, options.TotalBytes), w.NewBatch(), nil +} + +func (w *Writer) ExecuteBatch(batch store.KVBatch) error { + + emulatedBatch, ok := batch.(*store.EmulatedBatch) + if !ok { + return fmt.Errorf("wrong type of batch") + } + + w.s.m.Lock() + for k, mergeOps := range emulatedBatch.Merger.Merges { + kb := []byte(k) + var existingVal []byte + existingItem := w.s.t.Get(&Item{k: kb}) + if existingItem != nil { + existingVal = w.s.t.Get(&Item{k: kb}).(*Item).v + } + mergedVal, fullMergeOk := w.s.mo.FullMerge(kb, existingVal, mergeOps) + if !fullMergeOk { + return fmt.Errorf("merge operator returned failure") + } + w.s.t = w.s.t.Upsert(&Item{k: kb, v: mergedVal}, rand.Int()) + } + + for _, op := range emulatedBatch.Ops { + if op.V != nil { + w.s.t = w.s.t.Upsert(&Item{k: op.K, v: op.V}, rand.Int()) + } else { + w.s.t = w.s.t.Delete(&Item{k: op.K}) + } + } + w.s.m.Unlock() + + return nil +} + +func (w *Writer) Close() error { + w.s = nil + return nil +} diff --git a/index/upsidedown/store/metrics/batch.go b/index/upsidedown/store/metrics/batch.go new file mode 100644 index 0000000..8293509 --- /dev/null +++ b/index/upsidedown/store/metrics/batch.go @@ -0,0 +1,46 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import store "github.com/blevesearch/upsidedown_store_api" + +type Batch struct { + s *Store + o store.KVBatch +} + +func (b *Batch) Set(key, val []byte) { + b.o.Set(key, val) +} + +func (b *Batch) Delete(key []byte) { + b.o.Delete(key) +} + +func (b *Batch) Merge(key, val []byte) { + b.s.timerBatchMerge.Time(func() { + b.o.Merge(key, val) + }) +} + +func (b *Batch) Reset() { + b.o.Reset() +} + +func (b *Batch) Close() error { + err := b.o.Close() + b.o = nil + return err +} diff --git a/index/upsidedown/store/metrics/iterator.go b/index/upsidedown/store/metrics/iterator.go new file mode 100644 index 0000000..c55d2c3 --- /dev/null +++ b/index/upsidedown/store/metrics/iterator.go @@ -0,0 +1,58 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import store "github.com/blevesearch/upsidedown_store_api" + +type Iterator struct { + s *Store + o store.KVIterator +} + +func (i *Iterator) Seek(x []byte) { + i.s.timerIteratorSeek.Time(func() { + i.o.Seek(x) + }) +} + +func (i *Iterator) Next() { + i.s.timerIteratorNext.Time(func() { + i.o.Next() + }) +} + +func (i *Iterator) Current() ([]byte, []byte, bool) { + return i.o.Current() +} + +func (i *Iterator) Key() []byte { + return i.o.Key() +} + +func (i *Iterator) Value() []byte { + return i.o.Value() +} + +func (i *Iterator) Valid() bool { + return i.o.Valid() +} + +func (i *Iterator) Close() error { + err := i.o.Close() + if err != nil { + i.s.AddError("Iterator.Close", err, nil) + } + return err +} diff --git a/index/upsidedown/store/metrics/metrics_test.go b/index/upsidedown/store/metrics/metrics_test.go new file mode 100644 index 0000000..5ca5fb3 --- /dev/null +++ b/index/upsidedown/store/metrics/metrics_test.go @@ -0,0 +1,141 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" +) + +func TestMetricsStore(t *testing.T) { + _, err := New(nil, map[string]interface{}{}) + if err == nil { + t.Errorf("expected err when bad config") + } + + _, err = New(nil, map[string]interface{}{ + "kvStoreName_actual": "some-invalid-kvstore-name", + }) + if err == nil { + t.Errorf("expected err when unknown kvStoreName_actual") + } + + s, err := New(nil, map[string]interface{}{ + "kvStoreName_actual": gtreap.Name, + "path": "", + }) + if err != nil { + t.Fatal(err) + } + + b := bytes.NewBuffer(nil) + err = s.(*Store).WriteJSON(b) + if err != nil { + t.Fatal(err) + } + if b.Len() <= 0 { + t.Errorf("expected some output from WriteJSON") + } + var m map[string]interface{} + err = json.Unmarshal(b.Bytes(), &m) + if err != nil { + t.Errorf("expected WriteJSON to be unmarshallable") + } + if len(m) == 0 { + t.Errorf("expected some entries") + } + + b = bytes.NewBuffer(nil) + s.(*Store).WriteCSVHeader(b) + if b.Len() <= 0 { + t.Errorf("expected some output from WriteCSVHeader") + } + + b = bytes.NewBuffer(nil) + s.(*Store).WriteCSV(b) + if b.Len() <= 0 { + t.Errorf("expected some output from WriteCSV") + } +} + +func TestErrors(t *testing.T) { + s, err := New(nil, map[string]interface{}{ + "kvStoreName_actual": gtreap.Name, + "path": "", + }) + if err != nil { + t.Fatal(err) + } + + x, ok := s.(*Store) + if !ok { + t.Errorf("expecting a Store") + } + + x.AddError("foo", fmt.Errorf("Foo"), []byte("fooKey")) + x.AddError("bar", fmt.Errorf("Bar"), nil) + x.AddError("baz", fmt.Errorf("Baz"), []byte("bazKey")) + + b := bytes.NewBuffer(nil) + err = x.WriteJSON(b) + if err != nil { + t.Fatal(err) + } + + var m map[string]interface{} + err = json.Unmarshal(b.Bytes(), &m) + if err != nil { + t.Errorf("expected unmarshallable writeJSON, err: %v, b: %s", + err, b.Bytes()) + } + + errorsi, ok := m["Errors"] + if !ok || errorsi == nil { + t.Errorf("expected errorsi") + } + errors, ok := errorsi.([]interface{}) + if !ok || errors == nil { + t.Errorf("expected errorsi is array") + } + if len(errors) != 3 { + t.Errorf("expected errors len 3") + } + + e := errors[0].(map[string]interface{}) + if e["Op"].(string) != "foo" || + e["Err"].(string) != "Foo" || + len(e["Time"].(string)) < 10 || + e["Key"].(string) != "fooKey" { + t.Errorf("expected foo, %#v", e) + } + e = errors[1].(map[string]interface{}) + if e["Op"].(string) != "bar" || + e["Err"].(string) != "Bar" || + len(e["Time"].(string)) < 10 || + e["Key"].(string) != "" { + t.Errorf("expected bar, %#v", e) + } + e = errors[2].(map[string]interface{}) + if e["Op"].(string) != "baz" || + e["Err"].(string) != "Baz" || + len(e["Time"].(string)) < 10 || + e["Key"].(string) != "bazKey" { + t.Errorf("expected baz, %#v", e) + } +} diff --git a/index/upsidedown/store/metrics/reader.go b/index/upsidedown/store/metrics/reader.go new file mode 100644 index 0000000..ad1c25a --- /dev/null +++ b/index/upsidedown/store/metrics/reader.go @@ -0,0 +1,64 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import store "github.com/blevesearch/upsidedown_store_api" + +type Reader struct { + s *Store + o store.KVReader +} + +func (r *Reader) Get(key []byte) (v []byte, err error) { + r.s.timerReaderGet.Time(func() { + v, err = r.o.Get(key) + if err != nil { + r.s.AddError("Reader.Get", err, key) + } + }) + return +} + +func (r *Reader) MultiGet(keys [][]byte) (vals [][]byte, err error) { + r.s.timerReaderMultiGet.Time(func() { + vals, err = r.o.MultiGet(keys) + if err != nil { + r.s.AddError("Reader.MultiGet", err, nil) + } + }) + return +} + +func (r *Reader) PrefixIterator(prefix []byte) (i store.KVIterator) { + r.s.timerReaderPrefixIterator.Time(func() { + i = &Iterator{s: r.s, o: r.o.PrefixIterator(prefix)} + }) + return +} + +func (r *Reader) RangeIterator(start, end []byte) (i store.KVIterator) { + r.s.timerReaderRangeIterator.Time(func() { + i = &Iterator{s: r.s, o: r.o.RangeIterator(start, end)} + }) + return +} + +func (r *Reader) Close() error { + err := r.o.Close() + if err != nil { + r.s.AddError("Reader.Close", err, nil) + } + return err +} diff --git a/index/upsidedown/store/metrics/stats.go b/index/upsidedown/store/metrics/stats.go new file mode 100644 index 0000000..4888192 --- /dev/null +++ b/index/upsidedown/store/metrics/stats.go @@ -0,0 +1,50 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "github.com/blevesearch/bleve/v2/util" + store "github.com/blevesearch/upsidedown_store_api" +) + +type stats struct { + s *Store +} + +func (s *stats) statsMap() map[string]interface{} { + ms := map[string]interface{}{} + + ms["metrics"] = map[string]interface{}{ + "reader_get": TimerMap(s.s.timerReaderGet), + "reader_multi_get": TimerMap(s.s.timerReaderMultiGet), + "reader_prefix_iterator": TimerMap(s.s.timerReaderPrefixIterator), + "reader_range_iterator": TimerMap(s.s.timerReaderRangeIterator), + "writer_execute_batch": TimerMap(s.s.timerWriterExecuteBatch), + "iterator_seek": TimerMap(s.s.timerIteratorSeek), + "iterator_next": TimerMap(s.s.timerIteratorNext), + "batch_merge": TimerMap(s.s.timerBatchMerge), + } + + if o, ok := s.s.o.(store.KVStoreStats); ok { + ms["kv"] = o.StatsMap() + } + + return ms +} + +func (s *stats) MarshalJSON() ([]byte, error) { + m := s.statsMap() + return util.MarshalJSON(m) +} diff --git a/index/upsidedown/store/metrics/store.go b/index/upsidedown/store/metrics/store.go new file mode 100644 index 0000000..9a89756 --- /dev/null +++ b/index/upsidedown/store/metrics/store.go @@ -0,0 +1,277 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package metrics provides a bleve.store.KVStore implementation that +// wraps another, real KVStore implementation, and uses go-metrics to +// track runtime performance metrics. +package metrics + +import ( + "container/list" + "encoding/json" + "fmt" + "io" + "sync" + "time" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/util" + "github.com/blevesearch/go-metrics" + store "github.com/blevesearch/upsidedown_store_api" +) + +const Name = "metrics" + +type Store struct { + o store.KVStore + + timerReaderGet metrics.Timer + timerReaderMultiGet metrics.Timer + timerReaderPrefixIterator metrics.Timer + timerReaderRangeIterator metrics.Timer + timerWriterExecuteBatch metrics.Timer + timerIteratorSeek metrics.Timer + timerIteratorNext metrics.Timer + timerBatchMerge metrics.Timer + + m sync.Mutex // Protects the fields that follow. + errors *list.List // Capped list of StoreError's. + + s *stats +} + +func New(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) { + + name, ok := config["kvStoreName_actual"].(string) + if !ok || name == "" { + return nil, fmt.Errorf("metrics: missing kvStoreName_actual,"+ + " config: %#v", config) + } + + if name == Name { + return nil, fmt.Errorf("metrics: circular kvStoreName_actual") + } + + ctr := registry.KVStoreConstructorByName(name) + if ctr == nil { + return nil, fmt.Errorf("metrics: no kv store constructor,"+ + " kvStoreName_actual: %s", name) + } + + kvs, err := ctr(mo, config) + if err != nil { + return nil, err + } + + rv := &Store{ + o: kvs, + + timerReaderGet: metrics.NewTimer(), + timerReaderMultiGet: metrics.NewTimer(), + timerReaderPrefixIterator: metrics.NewTimer(), + timerReaderRangeIterator: metrics.NewTimer(), + timerWriterExecuteBatch: metrics.NewTimer(), + timerIteratorSeek: metrics.NewTimer(), + timerIteratorNext: metrics.NewTimer(), + timerBatchMerge: metrics.NewTimer(), + + errors: list.New(), + } + + rv.s = &stats{s: rv} + + return rv, nil +} + +func init() { + err := registry.RegisterKVStore(Name, New) + if err != nil { + panic(err) + } +} + +func (s *Store) Close() error { + return s.o.Close() +} + +func (s *Store) Reader() (store.KVReader, error) { + o, err := s.o.Reader() + if err != nil { + s.AddError("Reader", err, nil) + return nil, err + } + return &Reader{s: s, o: o}, nil +} + +func (s *Store) Writer() (store.KVWriter, error) { + o, err := s.o.Writer() + if err != nil { + s.AddError("Writer", err, nil) + return nil, err + } + return &Writer{s: s, o: o}, nil +} + +// Metric specific code below: + +const MaxErrors = 100 + +type StoreError struct { + Time string + Op string + Err string + Key string +} + +func (s *Store) AddError(op string, err error, key []byte) { + e := &StoreError{ + Time: time.Now().Format(time.RFC3339Nano), + Op: op, + Err: fmt.Sprintf("%v", err), + Key: string(key), + } + + s.m.Lock() + for s.errors.Len() >= MaxErrors { + s.errors.Remove(s.errors.Front()) + } + s.errors.PushBack(e) + s.m.Unlock() +} + +func (s *Store) WriteJSON(w io.Writer) (err error) { + _, err = w.Write([]byte(`{"TimerReaderGet":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerReaderGet) + _, err = w.Write([]byte(`,"TimerReaderMultiGet":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerReaderMultiGet) + _, err = w.Write([]byte(`,"TimerReaderPrefixIterator":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerReaderPrefixIterator) + _, err = w.Write([]byte(`,"TimerReaderRangeIterator":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerReaderRangeIterator) + _, err = w.Write([]byte(`,"TimerWriterExecuteBatch":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerWriterExecuteBatch) + _, err = w.Write([]byte(`,"TimerIteratorSeek":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerIteratorSeek) + _, err = w.Write([]byte(`,"TimerIteratorNext":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerIteratorNext) + _, err = w.Write([]byte(`,"TimerBatchMerge":`)) + if err != nil { + return + } + WriteTimerJSON(w, s.timerBatchMerge) + + _, err = w.Write([]byte(`,"Errors":[`)) + if err != nil { + return + } + s.m.Lock() + defer s.m.Unlock() + e := s.errors.Front() + i := 0 + for e != nil { + se, ok := e.Value.(*StoreError) + if ok && se != nil { + if i > 0 { + _, err = w.Write([]byte(",")) + if err != nil { + return + } + } + var buf []byte + buf, err = util.MarshalJSON(se) + if err == nil { + _, err = w.Write(buf) + if err != nil { + return + } + } + } + e = e.Next() + i = i + 1 + } + _, err = w.Write([]byte(`]`)) + if err != nil { + return + } + + // see if the underlying implementation has its own stats + if o, ok := s.o.(store.KVStoreStats); ok { + storeStats := o.Stats() + var storeBytes []byte + storeBytes, err = util.MarshalJSON(storeStats) + if err != nil { + return + } + _, err = fmt.Fprintf(w, `, "store": %s`, string(storeBytes)) + if err != nil { + return + } + } + + _, err = w.Write([]byte(`}`)) + if err != nil { + return + } + + return +} + +func (s *Store) WriteCSVHeader(w io.Writer) { + WriteTimerCSVHeader(w, "TimerReaderGet") + WriteTimerCSVHeader(w, "TimerReaderPrefixIterator") + WriteTimerCSVHeader(w, "TimerReaderRangeIterator") + WriteTimerCSVHeader(w, "TimerWtierExecuteBatch") + WriteTimerCSVHeader(w, "TimerIteratorSeek") + WriteTimerCSVHeader(w, "TimerIteratorNext") + WriteTimerCSVHeader(w, "TimerBatchMerge") +} + +func (s *Store) WriteCSV(w io.Writer) { + WriteTimerCSV(w, s.timerReaderGet) + WriteTimerCSV(w, s.timerReaderPrefixIterator) + WriteTimerCSV(w, s.timerReaderRangeIterator) + WriteTimerCSV(w, s.timerWriterExecuteBatch) + WriteTimerCSV(w, s.timerIteratorSeek) + WriteTimerCSV(w, s.timerIteratorNext) + WriteTimerCSV(w, s.timerBatchMerge) +} + +func (s *Store) Stats() json.Marshaler { + return s.s +} + +func (s *Store) StatsMap() map[string]interface{} { + return s.s.statsMap() +} diff --git a/index/upsidedown/store/metrics/store_test.go b/index/upsidedown/store/metrics/store_test.go new file mode 100644 index 0000000..bdcf8eb --- /dev/null +++ b/index/upsidedown/store/metrics/store_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + store "github.com/blevesearch/upsidedown_store_api" + "github.com/blevesearch/upsidedown_store_api/test" +) + +func open(t *testing.T, mo store.MergeOperator) store.KVStore { + rv, err := New(mo, map[string]interface{}{ + "kvStoreName_actual": gtreap.Name, + "path": "", + }) + if err != nil { + t.Fatal(err) + } + return rv +} + +func cleanup(t *testing.T, s store.KVStore) { + err := s.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestMetricsKVCrud(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestKVCrud(t, s) +} + +func TestMetricsReaderIsolation(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderIsolation(t, s) +} + +func TestMetricsReaderOwnsGetBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderOwnsGetBytes(t, s) +} + +func TestMetricsWriterOwnsBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestWriterOwnsBytes(t, s) +} + +func TestMetricsPrefixIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIterator(t, s) +} + +func TestMetricsPrefixIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIteratorSeek(t, s) +} + +func TestMetricsRangeIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIterator(t, s) +} + +func TestMetricsRangeIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIteratorSeek(t, s) +} + +func TestMetricsMerge(t *testing.T) { + s := open(t, &test.TestMergeCounter{}) + defer cleanup(t, s) + test.CommonTestMerge(t, s) +} diff --git a/index/upsidedown/store/metrics/util.go b/index/upsidedown/store/metrics/util.go new file mode 100644 index 0000000..6b41018 --- /dev/null +++ b/index/upsidedown/store/metrics/util.go @@ -0,0 +1,135 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "fmt" + "io" + "math" + + "github.com/blevesearch/go-metrics" +) + +// NOTE: This is copy & pasted from cbft as otherwise there +// would be an import cycle. + +var timerPercentiles = []float64{0.5, 0.75, 0.95, 0.99, 0.999} + +func TimerMap(timer metrics.Timer) map[string]interface{} { + + rv := make(map[string]interface{}) + t := timer.Snapshot() + p := t.Percentiles(timerPercentiles) + + percentileKeys := []string{"median", "75%", "95%", "99%", "99.9%"} + percentiles := make(map[string]interface{}) + for i, pi := range p { + if !isNanOrInf(pi) { + percentileKey := percentileKeys[i] + percentiles[percentileKey] = pi + } + } + + rateKeys := []string{"1-min", "5-min", "15-min", "mean"} + rates := make(map[string]interface{}) + for i, ri := range []float64{t.Rate1(), t.Rate5(), t.Rate15(), t.RateMean()} { + if !isNanOrInf(ri) { + rateKey := rateKeys[i] + rates[rateKey] = ri + } + } + + rv["count"] = t.Count() + rv["min"] = t.Min() + rv["max"] = t.Max() + mean := t.Mean() + if !isNanOrInf(mean) { + rv["mean"] = mean + } + stddev := t.StdDev() + if !isNanOrInf(stddev) { + rv["stddev"] = stddev + } + rv["percentiles"] = percentiles + rv["rates"] = rates + + return rv +} + +func isNanOrInf(v float64) bool { + if math.IsNaN(v) || math.IsInf(v, 0) { + return true + } + return false +} + +func WriteTimerJSON(w io.Writer, timer metrics.Timer) { + t := timer.Snapshot() + p := t.Percentiles(timerPercentiles) + + fmt.Fprintf(w, `{"count":%9d,`, t.Count()) + fmt.Fprintf(w, `"min":%9d,`, t.Min()) + fmt.Fprintf(w, `"max":%9d,`, t.Max()) + fmt.Fprintf(w, `"mean":%12.2f,`, t.Mean()) + fmt.Fprintf(w, `"stddev":%12.2f,`, t.StdDev()) + fmt.Fprintf(w, `"percentiles":{`) + fmt.Fprintf(w, `"median":%12.2f,`, p[0]) + fmt.Fprintf(w, `"75%%":%12.2f,`, p[1]) + fmt.Fprintf(w, `"95%%":%12.2f,`, p[2]) + fmt.Fprintf(w, `"99%%":%12.2f,`, p[3]) + fmt.Fprintf(w, `"99.9%%":%12.2f},`, p[4]) + fmt.Fprintf(w, `"rates":{`) + fmt.Fprintf(w, `"1-min":%12.2f,`, t.Rate1()) + fmt.Fprintf(w, `"5-min":%12.2f,`, t.Rate5()) + fmt.Fprintf(w, `"15-min":%12.2f,`, t.Rate15()) + fmt.Fprintf(w, `"mean":%12.2f}}`, t.RateMean()) +} + +func WriteTimerCSVHeader(w io.Writer, prefix string) { + fmt.Fprintf(w, "%s-count,", prefix) + fmt.Fprintf(w, "%s-min,", prefix) + fmt.Fprintf(w, "%s-max,", prefix) + fmt.Fprintf(w, "%s-mean,", prefix) + fmt.Fprintf(w, "%s-stddev,", prefix) + fmt.Fprintf(w, "%s-percentile-50%%,", prefix) + fmt.Fprintf(w, "%s-percentile-75%%,", prefix) + fmt.Fprintf(w, "%s-percentile-95%%,", prefix) + fmt.Fprintf(w, "%s-percentile-99%%,", prefix) + fmt.Fprintf(w, "%s-percentile-99.9%%,", prefix) + fmt.Fprintf(w, "%s-rate-1-min,", prefix) + fmt.Fprintf(w, "%s-rate-5-min,", prefix) + fmt.Fprintf(w, "%s-rate-15-min,", prefix) + fmt.Fprintf(w, "%s-rate-mean", prefix) +} + +func WriteTimerCSV(w io.Writer, timer metrics.Timer) { + t := timer.Snapshot() + p := t.Percentiles(timerPercentiles) + + fmt.Fprintf(w, `%d,`, t.Count()) + fmt.Fprintf(w, `%d,`, t.Min()) + fmt.Fprintf(w, `%d,`, t.Max()) + fmt.Fprintf(w, `%f,`, t.Mean()) + fmt.Fprintf(w, `%f,`, t.StdDev()) + fmt.Fprintf(w, `%f,`, p[0]) + fmt.Fprintf(w, `%f,`, p[1]) + fmt.Fprintf(w, `%f,`, p[2]) + fmt.Fprintf(w, `%f,`, p[3]) + fmt.Fprintf(w, `%f,`, p[4]) + fmt.Fprintf(w, `%f,`, t.Rate1()) + fmt.Fprintf(w, `%f,`, t.Rate5()) + fmt.Fprintf(w, `%f,`, t.Rate15()) + fmt.Fprintf(w, `%f`, t.RateMean()) +} diff --git a/index/upsidedown/store/metrics/writer.go b/index/upsidedown/store/metrics/writer.go new file mode 100644 index 0000000..c278e38 --- /dev/null +++ b/index/upsidedown/store/metrics/writer.go @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "fmt" + + store "github.com/blevesearch/upsidedown_store_api" +) + +type Writer struct { + s *Store + o store.KVWriter +} + +func (w *Writer) Close() error { + err := w.o.Close() + if err != nil { + w.s.AddError("Writer.Close", err, nil) + } + return err +} + +func (w *Writer) NewBatch() store.KVBatch { + return &Batch{s: w.s, o: w.o.NewBatch()} +} + +func (w *Writer) NewBatchEx(options store.KVBatchOptions) ([]byte, store.KVBatch, error) { + buf, b, err := w.o.NewBatchEx(options) + if err != nil { + return nil, nil, err + } + return buf, &Batch{s: w.s, o: b}, nil +} + +func (w *Writer) ExecuteBatch(b store.KVBatch) (err error) { + batch, ok := b.(*Batch) + if !ok { + return fmt.Errorf("wrong type of batch") + } + w.s.timerWriterExecuteBatch.Time(func() { + err = w.o.ExecuteBatch(batch.o) + if err != nil { + w.s.AddError("Writer.ExecuteBatch", err, nil) + } + }) + return +} diff --git a/index/upsidedown/store/moss/batch.go b/index/upsidedown/store/moss/batch.go new file mode 100644 index 0000000..0aa998a --- /dev/null +++ b/index/upsidedown/store/moss/batch.go @@ -0,0 +1,87 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moss + +import ( + "github.com/couchbase/moss" + + store "github.com/blevesearch/upsidedown_store_api" +) + +type Batch struct { + store *Store + merge *store.EmulatedMerge + batch moss.Batch + buf []byte // Non-nil when using pre-alloc'ed / NewBatchEx(). + bufUsed int +} + +func (b *Batch) Set(key, val []byte) { + var err error + if b.buf != nil { + b.bufUsed += len(key) + len(val) + err = b.batch.AllocSet(key, val) + } else { + err = b.batch.Set(key, val) + } + + if err != nil { + b.store.Logf("bleve moss batch.Set err: %v", err) + } +} + +func (b *Batch) Delete(key []byte) { + var err error + if b.buf != nil { + b.bufUsed += len(key) + err = b.batch.AllocDel(key) + } else { + err = b.batch.Del(key) + } + + if err != nil { + b.store.Logf("bleve moss batch.Delete err: %v", err) + } +} + +func (b *Batch) Merge(key, val []byte) { + if b.buf != nil { + b.bufUsed += len(key) + len(val) + } + b.merge.Merge(key, val) +} + +func (b *Batch) Reset() { + err := b.Close() + if err != nil { + b.store.Logf("bleve moss batch.Close err: %v", err) + return + } + + batch, err := b.store.ms.NewBatch(0, 0) + if err == nil { + b.batch = batch + b.merge = store.NewEmulatedMerge(b.store.mo) + b.buf = nil + b.bufUsed = 0 + } +} + +func (b *Batch) Close() error { + b.merge = nil + err := b.batch.Close() + b.batch = nil + return err +} diff --git a/index/upsidedown/store/moss/iterator.go b/index/upsidedown/store/moss/iterator.go new file mode 100644 index 0000000..756c383 --- /dev/null +++ b/index/upsidedown/store/moss/iterator.go @@ -0,0 +1,87 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moss + +import ( + "github.com/couchbase/moss" +) + +type Iterator struct { + store *Store + ss moss.Snapshot + iter moss.Iterator + start []byte + end []byte + k []byte + v []byte + err error +} + +func (x *Iterator) Seek(seekToKey []byte) { + _ = x.iter.SeekTo(seekToKey) + + x.k, x.v, x.err = x.iter.Current() +} + +func (x *Iterator) Next() { + _ = x.iter.Next() + + x.k, x.v, x.err = x.iter.Current() +} + +func (x *Iterator) Current() ([]byte, []byte, bool) { + return x.k, x.v, x.err == nil +} + +func (x *Iterator) Key() []byte { + if x.err != nil { + return nil + } + + return x.k +} + +func (x *Iterator) Value() []byte { + if x.err != nil { + return nil + } + + return x.v +} + +func (x *Iterator) Valid() bool { + return x.err == nil +} + +func (x *Iterator) Close() error { + var err error + + x.ss = nil + + if x.iter != nil { + err = x.iter.Close() + x.iter = nil + } + + x.k = nil + x.v = nil + x.err = moss.ErrIteratorDone + + return err +} + +func (x *Iterator) current() { + x.k, x.v, x.err = x.iter.Current() +} diff --git a/index/upsidedown/store/moss/lower.go b/index/upsidedown/store/moss/lower.go new file mode 100644 index 0000000..d52bc30 --- /dev/null +++ b/index/upsidedown/store/moss/lower.go @@ -0,0 +1,571 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package moss provides a KVStore implementation based on the +// github.com/couchbase/moss library. + +package moss + +import ( + "fmt" + "os" + "sync" + + "github.com/couchbase/moss" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/util" + store "github.com/blevesearch/upsidedown_store_api" +) + +func initLowerLevelStore( + config map[string]interface{}, + lowerLevelStoreName string, + lowerLevelStoreConfig map[string]interface{}, + lowerLevelMaxBatchSize uint64, + options moss.CollectionOptions, +) (moss.Snapshot, moss.LowerLevelUpdate, store.KVStore, statsFunc, error) { + if lowerLevelStoreConfig == nil { + lowerLevelStoreConfig = map[string]interface{}{} + } + + for k, v := range config { + _, exists := lowerLevelStoreConfig[k] + if !exists { + lowerLevelStoreConfig[k] = v + } + } + + if lowerLevelStoreName == "mossStore" { + return InitMossStore(lowerLevelStoreConfig, options) + } + + constructor := registry.KVStoreConstructorByName(lowerLevelStoreName) + if constructor == nil { + return nil, nil, nil, nil, fmt.Errorf("moss store, initLowerLevelStore,"+ + " could not find lower level store: %s", lowerLevelStoreName) + } + + kvStore, err := constructor(options.MergeOperator, lowerLevelStoreConfig) + if err != nil { + return nil, nil, nil, nil, err + } + + llStore := &llStore{ + refs: 0, + config: config, + llConfig: lowerLevelStoreConfig, + kvStore: kvStore, + logf: options.Log, + } + + llUpdate := func(ssHigher moss.Snapshot) (ssLower moss.Snapshot, err error) { + return llStore.update(ssHigher, lowerLevelMaxBatchSize) + } + + llSnapshot, err := llUpdate(nil) + if err != nil { + _ = kvStore.Close() + return nil, nil, nil, nil, err + } + + return llSnapshot, llUpdate, kvStore, nil, nil // llStore.refs is now 1. +} + +// ------------------------------------------------ + +// llStore is a lower level store and provides ref-counting around a +// bleve store.KVStore. +type llStore struct { + kvStore store.KVStore + + config map[string]interface{} + llConfig map[string]interface{} + + logf func(format string, a ...interface{}) + + m sync.Mutex // Protects fields that follow. + refs int +} + +// llSnapshot represents a lower-level snapshot, wrapping a bleve +// store.KVReader, and implements the moss.Snapshot interface. +type llSnapshot struct { + llStore *llStore // Holds 1 refs on the llStore. + kvReader store.KVReader + childSnapshots map[string]*llSnapshot + + m sync.Mutex // Protects fields that follow. + refs int +} + +// llIterator represents a lower-level iterator, wrapping a bleve +// store.KVIterator, and implements the moss.Iterator interface. +type llIterator struct { + llSnapshot *llSnapshot // Holds 1 refs on the llSnapshot. + + // Some lower-level KVReader implementations need a separate + // KVReader clone, due to KVReader single-threaded'ness. + kvReader store.KVReader + + kvIterator store.KVIterator +} + +type readerSource interface { + Reader() (store.KVReader, error) +} + +// ------------------------------------------------ + +func (s *llStore) addRef() *llStore { + s.m.Lock() + s.refs += 1 + s.m.Unlock() + + return s +} + +func (s *llStore) decRef() { + s.m.Lock() + s.refs -= 1 + if s.refs <= 0 { + err := s.kvStore.Close() + if err != nil { + s.logf("llStore kvStore.Close err: %v", err) + } + } + s.m.Unlock() +} + +// update() mutates this lower level store with latest data from the +// given higher level moss.Snapshot and returns a new moss.Snapshot +// that the higher level can use which represents this lower level +// store. +func (s *llStore) update(ssHigher moss.Snapshot, maxBatchSize uint64) ( + ssLower moss.Snapshot, err error, +) { + if ssHigher != nil { + iter, err := ssHigher.StartIterator(nil, nil, moss.IteratorOptions{ + IncludeDeletions: true, + SkipLowerLevel: true, + }) + if err != nil { + return nil, err + } + + defer func() { + err = iter.Close() + if err != nil { + s.logf("llStore iter.Close err: %v", err) + } + }() + + kvWriter, err := s.kvStore.Writer() + if err != nil { + return nil, err + } + + defer func() { + err = kvWriter.Close() + if err != nil { + s.logf("llStore kvWriter.Close err: %v", err) + } + }() + + batch := kvWriter.NewBatch() + + defer func() { + if batch != nil { + err = batch.Close() + if err != nil { + s.logf("llStore batch.Close err: %v", err) + } + } + }() + + var readOptions moss.ReadOptions + + i := uint64(0) + for { + if i%1000000 == 0 { + s.logf("llStore.update, i: %d", i) + } + + ex, key, val, err := iter.CurrentEx() + if err == moss.ErrIteratorDone { + break + } + if err != nil { + return nil, err + } + + switch ex.Operation { + case moss.OperationSet: + batch.Set(key, val) + + case moss.OperationDel: + batch.Delete(key) + + case moss.OperationMerge: + val, err = ssHigher.Get(key, readOptions) + if err != nil { + return nil, err + } + + if val != nil { + batch.Set(key, val) + } else { + batch.Delete(key) + } + + default: + return nil, fmt.Errorf("moss store, update,"+ + " unexpected operation, ex: %v", ex) + } + + i++ + + err = iter.Next() + if err == moss.ErrIteratorDone { + break + } + if err != nil { + return nil, err + } + + if maxBatchSize > 0 && i%maxBatchSize == 0 { + err = kvWriter.ExecuteBatch(batch) + if err != nil { + return nil, err + } + + err = batch.Close() + if err != nil { + return nil, err + } + + batch = kvWriter.NewBatch() + } + } + + if i > 0 { + s.logf("llStore.update, ExecuteBatch,"+ + " path: %s, total: %d, start", s.llConfig["path"], i) + + err = kvWriter.ExecuteBatch(batch) + if err != nil { + return nil, err + } + + s.logf("llStore.update, ExecuteBatch,"+ + " path: %s: total: %d, done", s.llConfig["path"], i) + } + } + + kvReader, err := s.kvStore.Reader() + if err != nil { + return nil, err + } + + s.logf("llStore.update, new reader") + + return &llSnapshot{ + llStore: s.addRef(), + kvReader: kvReader, + refs: 1, + }, nil +} + +// ------------------------------------------------ + +func (llss *llSnapshot) addRef() *llSnapshot { + llss.m.Lock() + llss.refs += 1 + llss.m.Unlock() + + return llss +} + +func (llss *llSnapshot) decRef() { + llss.m.Lock() + llss.refs -= 1 + if llss.refs <= 0 { + if llss.kvReader != nil { + err := llss.kvReader.Close() + if err != nil { + llss.llStore.logf("llSnapshot kvReader.Close err: %v", err) + } + + llss.kvReader = nil + } + + if llss.llStore != nil { + llss.llStore.decRef() + llss.llStore = nil + } + } + llss.m.Unlock() +} + +// ChildCollectionNames returns an array of child collection name strings. +func (llss *llSnapshot) ChildCollectionNames() ([]string, error) { + childCollections := make([]string, len(llss.childSnapshots)) + idx := 0 + for name := range llss.childSnapshots { + childCollections[idx] = name + idx++ + } + return childCollections, nil +} + +// ChildCollectionSnapshot returns a Snapshot on a given child +// collection by its name. +func (llss *llSnapshot) ChildCollectionSnapshot(childCollectionName string) ( + moss.Snapshot, error, +) { + childSnapshot, exists := llss.childSnapshots[childCollectionName] + if !exists { + return nil, nil + } + childSnapshot.addRef() + return childSnapshot, nil +} + +func (llss *llSnapshot) Close() error { + llss.decRef() + + return nil +} + +func (llss *llSnapshot) Get(key []byte, + readOptions moss.ReadOptions, +) ([]byte, error) { + rs, ok := llss.kvReader.(readerSource) + if ok { + r2, err := rs.Reader() + if err != nil { + return nil, err + } + + val, err := r2.Get(key) + + _ = r2.Close() + + return val, err + } + + return llss.kvReader.Get(key) +} + +func (llss *llSnapshot) StartIterator( + startKeyInclusive, endKeyExclusive []byte, + iteratorOptions moss.IteratorOptions, +) (moss.Iterator, error) { + rs, ok := llss.kvReader.(readerSource) + if ok { + r2, err := rs.Reader() + if err != nil { + return nil, err + } + + i2 := r2.RangeIterator(startKeyInclusive, endKeyExclusive) + + return &llIterator{llSnapshot: llss.addRef(), kvReader: r2, kvIterator: i2}, nil + } + + i := llss.kvReader.RangeIterator(startKeyInclusive, endKeyExclusive) + + return &llIterator{llSnapshot: llss.addRef(), kvReader: nil, kvIterator: i}, nil +} + +// ------------------------------------------------ + +func (lli *llIterator) Close() error { + var err0 error + if lli.kvIterator != nil { + err0 = lli.kvIterator.Close() + lli.kvIterator = nil + } + + var err1 error + if lli.kvReader != nil { + err1 = lli.kvReader.Close() + lli.kvReader = nil + } + + lli.llSnapshot.decRef() + lli.llSnapshot = nil + + if err0 != nil { + return err0 + } + + if err1 != nil { + return err1 + } + + return nil +} + +func (lli *llIterator) Next() error { + lli.kvIterator.Next() + + return nil +} + +func (lli *llIterator) SeekTo(k []byte) error { + lli.kvIterator.Seek(k) + + return nil +} + +func (lli *llIterator) Current() (key, val []byte, err error) { + key, val, ok := lli.kvIterator.Current() + if !ok { + return nil, nil, moss.ErrIteratorDone + } + + return key, val, nil +} + +func (lli *llIterator) CurrentEx() ( + entryEx moss.EntryEx, key, val []byte, err error, +) { + return moss.EntryEx{}, nil, nil, moss.ErrUnimplemented +} + +// ------------------------------------------------ + +func InitMossStore(config map[string]interface{}, options moss.CollectionOptions) ( + moss.Snapshot, moss.LowerLevelUpdate, store.KVStore, statsFunc, error, +) { + path, ok := config["path"].(string) + if !ok { + return nil, nil, nil, nil, fmt.Errorf("lower: missing path for InitMossStore config") + } + if path == "" { + return nil, nil, nil, nil, os.ErrInvalid + } + + err := os.MkdirAll(path, 0o700) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("lower: InitMossStore mkdir,"+ + " path: %s, err: %v", path, err) + } + + storeOptions := moss.StoreOptions{ + CollectionOptions: options, + } + v, ok := config["mossStoreOptions"] + if ok { + b, err := util.MarshalJSON(v) // Convert from map[string]interface{}. + if err != nil { + return nil, nil, nil, nil, err + } + + err = util.UnmarshalJSON(b, &storeOptions) + if err != nil { + return nil, nil, nil, nil, err + } + } + + s, err := moss.OpenStore(path, storeOptions) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("lower: moss.OpenStore,"+ + " path: %s, err: %v", path, err) + } + + sw := &mossStoreWrapper{s: s} + + llUpdate := func(ssHigher moss.Snapshot) (moss.Snapshot, error) { + ss, err := sw.s.Persist(ssHigher, moss.StorePersistOptions{ + CompactionConcern: moss.CompactionAllow, + }) + if err != nil { + return nil, err + } + + sw.AddRef() // Ref-count to be owned by snapshot wrapper. + + return moss.NewSnapshotWrapper(ss, sw), nil + } + + llSnapshot, err := llUpdate(nil) + if err != nil { + _ = s.Close() + return nil, nil, nil, nil, err + } + + llStats := func() map[string]interface{} { + stats, err := s.Stats() + if err != nil { + return nil + } + return stats + } + + return llSnapshot, llUpdate, sw, llStats, nil +} + +// mossStoreWrapper implements the bleve.index.store.KVStore +// interface, but only barely enough to allow it to be passed around +// as a lower-level store. Advanced apps will likely cast the +// mossStoreWrapper to access the Actual() method. +type mossStoreWrapper struct { + m sync.Mutex + refs int + s *moss.Store +} + +func (w *mossStoreWrapper) AddRef() { + w.m.Lock() + w.refs++ + w.m.Unlock() +} + +func (w *mossStoreWrapper) Close() (err error) { + w.m.Lock() + w.refs-- + if w.refs <= 0 { + err = w.s.Close() + w.s = nil + } + w.m.Unlock() + return err +} + +func (w *mossStoreWrapper) Reader() (store.KVReader, error) { + return nil, fmt.Errorf("unexpected") +} + +func (w *mossStoreWrapper) Writer() (store.KVWriter, error) { + return nil, fmt.Errorf("unexpected") +} + +func (w *mossStoreWrapper) Actual() *moss.Store { + w.m.Lock() + rv := w.s + w.m.Unlock() + return rv +} + +func (w *mossStoreWrapper) histograms() string { + var rv string + w.m.Lock() + if w.s != nil { + rv = w.s.Histograms().String() + } + w.m.Unlock() + return rv +} diff --git a/index/upsidedown/store/moss/lower_test.go b/index/upsidedown/store/moss/lower_test.go new file mode 100644 index 0000000..85afe49 --- /dev/null +++ b/index/upsidedown/store/moss/lower_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moss + +import ( + "os" + "testing" + + store "github.com/blevesearch/upsidedown_store_api" + "github.com/blevesearch/upsidedown_store_api/test" +) + +func openWithLower(t *testing.T, mo store.MergeOperator) (string, store.KVStore) { + tmpDir, _ := os.MkdirTemp("", "mossStore") + + config := map[string]interface{}{ + "path": tmpDir, + "mossLowerLevelStoreName": "mossStore", + } + + rv, err := New(mo, config) + if err != nil { + t.Fatal(err) + } + return tmpDir, rv +} + +func cleanupWithLower(t *testing.T, s store.KVStore, tmpDir string) { + err := s.Close() + if err != nil { + t.Fatal(err) + } + err = os.RemoveAll(tmpDir) + if err != nil { + t.Fatal(err) + } +} + +func TestMossWithLowerKVCrud(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestKVCrud(t, s) +} + +func TestMossWithLowerReaderIsolation(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestReaderIsolation(t, s) +} + +func TestMossWithLowerReaderOwnsGetBytes(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestReaderOwnsGetBytes(t, s) +} + +func TestMossWithLowerWriterOwnsBytes(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestWriterOwnsBytes(t, s) +} + +func TestMossWithLowerPrefixIterator(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestPrefixIterator(t, s) +} + +func TestMossWithLowerPrefixIteratorSeek(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestPrefixIteratorSeek(t, s) +} + +func TestMossWithLowerRangeIterator(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestRangeIterator(t, s) +} + +func TestMossWithLowerRangeIteratorSeek(t *testing.T) { + tmpDir, s := openWithLower(t, nil) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestRangeIteratorSeek(t, s) +} + +func TestMossWithLowerMerge(t *testing.T) { + tmpDir, s := openWithLower(t, &test.TestMergeCounter{}) + defer cleanupWithLower(t, s, tmpDir) + test.CommonTestMerge(t, s) +} diff --git a/index/upsidedown/store/moss/reader.go b/index/upsidedown/store/moss/reader.go new file mode 100644 index 0000000..93063cd --- /dev/null +++ b/index/upsidedown/store/moss/reader.go @@ -0,0 +1,97 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moss + +import ( + "github.com/couchbase/moss" + + store "github.com/blevesearch/upsidedown_store_api" +) + +type Reader struct { + store *Store + ss moss.Snapshot +} + +func (r *Reader) Get(k []byte) (v []byte, err error) { + v, err = r.ss.Get(k, moss.ReadOptions{}) + if err != nil { + return nil, err + } + if v != nil { + return append(make([]byte, 0, len(v)), v...), nil + } + return nil, nil +} + +func (r *Reader) MultiGet(keys [][]byte) ([][]byte, error) { + return store.MultiGet(r, keys) +} + +func (r *Reader) PrefixIterator(k []byte) store.KVIterator { + kEnd := incrementBytes(k) + + iter, err := r.ss.StartIterator(k, kEnd, moss.IteratorOptions{}) + if err != nil { + return nil + } + + rv := &Iterator{ + store: r.store, + ss: r.ss, + iter: iter, + start: k, + end: kEnd, + } + + rv.current() + + return rv +} + +func (r *Reader) RangeIterator(start, end []byte) store.KVIterator { + iter, err := r.ss.StartIterator(start, end, moss.IteratorOptions{}) + if err != nil { + return nil + } + + rv := &Iterator{ + store: r.store, + ss: r.ss, + iter: iter, + start: start, + end: end, + } + + rv.current() + + return rv +} + +func (r *Reader) Close() error { + return r.ss.Close() +} + +func incrementBytes(in []byte) []byte { + rv := make([]byte, len(in)) + copy(rv, in) + for i := len(rv) - 1; i >= 0; i-- { + rv[i] = rv[i] + 1 + if rv[i] != 0 { + return rv // didn't overflow, so stop + } + } + return nil // overflowed +} diff --git a/index/upsidedown/store/moss/stats.go b/index/upsidedown/store/moss/stats.go new file mode 100644 index 0000000..3956bbd --- /dev/null +++ b/index/upsidedown/store/moss/stats.go @@ -0,0 +1,58 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moss + +import ( + "github.com/blevesearch/bleve/v2/util" + store "github.com/blevesearch/upsidedown_store_api" +) + +type stats struct { + s *Store +} + +func (s *stats) statsMap() map[string]interface{} { + ms := map[string]interface{}{} + + var err error + ms["moss"], err = s.s.ms.Stats() + if err != nil { + return ms + } + + if s.s.llstore != nil { + if o, ok := s.s.llstore.(store.KVStoreStats); ok { + ms["kv"] = o.StatsMap() + } + } + + _, exists := ms["kv"] + if !exists && s.s.llstats != nil { + ms["kv"] = s.s.llstats() + } + + if msw, ok := s.s.llstore.(*mossStoreWrapper); ok { + ms["store_histograms"] = msw.histograms() + } + + ms["coll_histograms"] = s.s.ms.Histograms().String() + + return ms +} + +func (s *stats) MarshalJSON() ([]byte, error) { + m := s.statsMap() + return util.MarshalJSON(m) +} diff --git a/index/upsidedown/store/moss/store.go b/index/upsidedown/store/moss/store.go new file mode 100644 index 0000000..bccbe1f --- /dev/null +++ b/index/upsidedown/store/moss/store.go @@ -0,0 +1,231 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package moss provides a KVStore implementation based on the +// github.com/couchbase/moss library. + +package moss + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/couchbase/moss" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/util" + store "github.com/blevesearch/upsidedown_store_api" +) + +// RegistryCollectionOptions should be treated as read-only after +// process init()'ialization. +var RegistryCollectionOptions = map[string]moss.CollectionOptions{} + +const Name = "moss" + +type Store struct { + m sync.Mutex + ms moss.Collection + mo store.MergeOperator + llstore store.KVStore // May be nil. + llstats statsFunc // May be nil. + + s *stats + config map[string]interface{} +} + +type statsFunc func() map[string]interface{} + +// New initializes a moss storage with values from the optional +// config["mossCollectionOptions"] (a JSON moss.CollectionOptions). +// Next, values from the RegistryCollectionOptions, named by the +// optional config["mossCollectionOptionsName"], take precedence. +// Finally, base case defaults are taken from +// moss.DefaultCollectionOptions. +func New(mo store.MergeOperator, config map[string]interface{}) ( + store.KVStore, error) { + options := moss.DefaultCollectionOptions // Copy. + + v, ok := config["mossCollectionOptionsName"] + if ok { + name, ok := v.(string) + if !ok { + return nil, fmt.Errorf("moss store,"+ + " could not parse config[mossCollectionOptionsName]: %v", v) + } + + options, ok = RegistryCollectionOptions[name] // Copy. + if !ok { + return nil, fmt.Errorf("moss store,"+ + " could not find RegistryCollectionOptions, name: %s", name) + } + } + + options.MergeOperator = mo + options.DeferredSort = true + + v, ok = config["mossCollectionOptions"] + if ok { + b, err := util.MarshalJSON(v) // Convert from map[string]interface{}. + if err != nil { + return nil, fmt.Errorf("moss store,"+ + " could not marshal config[mossCollectionOptions]: %v, err: %v", v, err) + } + + err = util.UnmarshalJSON(b, &options) + if err != nil { + return nil, fmt.Errorf("moss store,"+ + " could not unmarshal config[mossCollectionOptions]: %v, err: %v", v, err) + } + } + + // -------------------------------------------------- + + if options.Log == nil || options.Debug <= 0 { + options.Log = func(format string, a ...interface{}) {} + } + + // -------------------------------------------------- + + mossLowerLevelStoreName := "" + v, ok = config["mossLowerLevelStoreName"] + if ok { + mossLowerLevelStoreName, ok = v.(string) + if !ok { + return nil, fmt.Errorf("moss store,"+ + " could not parse config[mossLowerLevelStoreName]: %v", v) + } + } + + var llStore store.KVStore + var llStats statsFunc + + if options.LowerLevelInit == nil && + options.LowerLevelUpdate == nil && + mossLowerLevelStoreName != "" { + mossLowerLevelStoreConfig := map[string]interface{}{} + v, ok := config["mossLowerLevelStoreConfig"] + if ok { + mossLowerLevelStoreConfig, ok = v.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("moss store, initLowerLevelStore,"+ + " could parse mossLowerLevelStoreConfig: %v", v) + } + } + + mossLowerLevelMaxBatchSize := uint64(0) + v, ok = config["mossLowerLevelMaxBatchSize"] + if ok { + mossLowerLevelMaxBatchSizeF, ok := v.(float64) + if !ok { + return nil, fmt.Errorf("moss store,"+ + " could not parse config[mossLowerLevelMaxBatchSize]: %v", v) + } + + mossLowerLevelMaxBatchSize = uint64(mossLowerLevelMaxBatchSizeF) + } + + lowerLevelInit, lowerLevelUpdate, lowerLevelStore, lowerLevelStats, err := + initLowerLevelStore(config, + mossLowerLevelStoreName, + mossLowerLevelStoreConfig, + mossLowerLevelMaxBatchSize, + options) + if err != nil { + return nil, err + } + + options.LowerLevelInit = lowerLevelInit + options.LowerLevelUpdate = lowerLevelUpdate + + llStore = lowerLevelStore + llStats = lowerLevelStats + } + + // -------------------------------------------------- + + ms, err := moss.NewCollection(options) + if err != nil { + return nil, err + } + err = ms.Start() + if err != nil { + return nil, err + } + rv := Store{ + ms: ms, + mo: mo, + llstore: llStore, + llstats: llStats, + config: config, + } + rv.s = &stats{s: &rv} + return &rv, nil +} + +func (s *Store) Close() error { + if val, ok := s.config["mossAbortCloseEnabled"]; ok { + if v, ok := val.(bool); ok && v { + if msw, ok := s.llstore.(*mossStoreWrapper); ok { + if s := msw.Actual(); s != nil { + _ = s.CloseEx(moss.StoreCloseExOptions{Abort: true}) + } + } + } + } + return s.ms.Close() +} + +func (s *Store) Reader() (store.KVReader, error) { + ss, err := s.ms.Snapshot() + if err != nil { + return nil, err + } + return &Reader{ss: ss}, nil +} + +func (s *Store) Writer() (store.KVWriter, error) { + return &Writer{s: s}, nil +} + +func (s *Store) Logf(fmt string, args ...interface{}) { + options := s.ms.Options() + if options.Log != nil { + options.Log(fmt, args...) + } +} + +func (s *Store) Stats() json.Marshaler { + return s.s +} + +func (s *Store) StatsMap() map[string]interface{} { + return s.s.statsMap() +} + +func (s *Store) LowerLevelStore() store.KVStore { + return s.llstore +} + +func (s *Store) Collection() moss.Collection { + return s.ms +} + +func init() { + err := registry.RegisterKVStore(Name, New) + if err != nil { + panic(err) + } +} diff --git a/index/upsidedown/store/moss/store_test.go b/index/upsidedown/store/moss/store_test.go new file mode 100644 index 0000000..49013b6 --- /dev/null +++ b/index/upsidedown/store/moss/store_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moss + +import ( + "testing" + + store "github.com/blevesearch/upsidedown_store_api" + "github.com/blevesearch/upsidedown_store_api/test" +) + +func open(t *testing.T, mo store.MergeOperator) store.KVStore { + rv, err := New(mo, nil) + if err != nil { + t.Fatal(err) + } + return rv +} + +func cleanup(t *testing.T, s store.KVStore) { + err := s.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestMossKVCrud(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestKVCrud(t, s) +} + +func TestMossReaderIsolation(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderIsolation(t, s) +} + +func TestMossReaderOwnsGetBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestReaderOwnsGetBytes(t, s) +} + +func TestMossWriterOwnsBytes(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestWriterOwnsBytes(t, s) +} + +func TestMossPrefixIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIterator(t, s) +} + +func TestMossPrefixIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestPrefixIteratorSeek(t, s) +} + +func TestMossRangeIterator(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIterator(t, s) +} + +func TestMossRangeIteratorSeek(t *testing.T) { + s := open(t, nil) + defer cleanup(t, s) + test.CommonTestRangeIteratorSeek(t, s) +} + +func TestMossMerge(t *testing.T) { + s := open(t, &test.TestMergeCounter{}) + defer cleanup(t, s) + test.CommonTestMerge(t, s) +} diff --git a/index/upsidedown/store/moss/writer.go b/index/upsidedown/store/moss/writer.go new file mode 100644 index 0000000..cb44716 --- /dev/null +++ b/index/upsidedown/store/moss/writer.go @@ -0,0 +1,97 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package moss + +import ( + "fmt" + + store "github.com/blevesearch/upsidedown_store_api" + + "github.com/couchbase/moss" +) + +type Writer struct { + s *Store +} + +func (w *Writer) NewBatch() store.KVBatch { + b, err := w.s.ms.NewBatch(0, 0) + if err != nil { + return nil + } + + return &Batch{ + store: w.s, + merge: store.NewEmulatedMerge(w.s.mo), + batch: b, + } +} + +func (w *Writer) NewBatchEx(options store.KVBatchOptions) ( + []byte, store.KVBatch, error) { + numOps := options.NumSets + options.NumDeletes + options.NumMerges + + b, err := w.s.ms.NewBatch(numOps, options.TotalBytes) + if err != nil { + return nil, nil, err + } + + buf, err := b.Alloc(options.TotalBytes) + if err != nil { + return nil, nil, err + } + + return buf, &Batch{ + store: w.s, + merge: store.NewEmulatedMerge(w.s.mo), + batch: b, + buf: buf, + bufUsed: 0, + }, nil +} + +func (w *Writer) ExecuteBatch(b store.KVBatch) (err error) { + batch, ok := b.(*Batch) + if !ok { + return fmt.Errorf("wrong type of batch") + } + + for kStr, mergeOps := range batch.merge.Merges { + for _, v := range mergeOps { + if batch.buf != nil { + kLen := len(kStr) + vLen := len(v) + kBuf := batch.buf[batch.bufUsed : batch.bufUsed+kLen] + vBuf := batch.buf[batch.bufUsed+kLen : batch.bufUsed+kLen+vLen] + copy(kBuf, kStr) + copy(vBuf, v) + batch.bufUsed += kLen + vLen + err = batch.batch.AllocMerge(kBuf, vBuf) + } else { + err = batch.batch.Merge([]byte(kStr), v) + } + if err != nil { + return err + } + } + } + + return w.s.ms.ExecuteBatch(batch.batch, moss.WriteOptions{}) +} + +func (w *Writer) Close() error { + w.s = nil + return nil +} diff --git a/index/upsidedown/store/null/null.go b/index/upsidedown/store/null/null.go new file mode 100644 index 0000000..952e1eb --- /dev/null +++ b/index/upsidedown/store/null/null.go @@ -0,0 +1,121 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package null + +import ( + "github.com/blevesearch/bleve/v2/registry" + store "github.com/blevesearch/upsidedown_store_api" +) + +const Name = "null" + +type Store struct{} + +func New(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) { + return &Store{}, nil +} + +func (i *Store) Close() error { + return nil +} + +func (i *Store) Reader() (store.KVReader, error) { + return &reader{}, nil +} + +func (i *Store) Writer() (store.KVWriter, error) { + return &writer{}, nil +} + +type reader struct{} + +func (r *reader) Get(key []byte) ([]byte, error) { + return nil, nil +} + +func (r *reader) MultiGet(keys [][]byte) ([][]byte, error) { + return make([][]byte, len(keys)), nil +} + +func (r *reader) PrefixIterator(prefix []byte) store.KVIterator { + return &iterator{} +} + +func (r *reader) RangeIterator(start, end []byte) store.KVIterator { + return &iterator{} +} + +func (r *reader) Close() error { + return nil +} + +type iterator struct{} + +func (i *iterator) SeekFirst() {} +func (i *iterator) Seek(k []byte) {} +func (i *iterator) Next() {} + +func (i *iterator) Current() ([]byte, []byte, bool) { + return nil, nil, false +} + +func (i *iterator) Key() []byte { + return nil +} + +func (i *iterator) Value() []byte { + return nil +} + +func (i *iterator) Valid() bool { + return false +} + +func (i *iterator) Close() error { + return nil +} + +type batch struct{} + +func (i *batch) Set(key, val []byte) {} +func (i *batch) Delete(key []byte) {} +func (i *batch) Merge(key, val []byte) {} +func (i *batch) Reset() {} +func (i *batch) Close() error { return nil } + +type writer struct{} + +func (w *writer) NewBatch() store.KVBatch { + return &batch{} +} + +func (w *writer) NewBatchEx(options store.KVBatchOptions) ([]byte, store.KVBatch, error) { + return make([]byte, options.TotalBytes), w.NewBatch(), nil +} + +func (w *writer) ExecuteBatch(store.KVBatch) error { + return nil +} + +func (w *writer) Close() error { + return nil +} + +func init() { + err := registry.RegisterKVStore(Name, New) + if err != nil { + panic(err) + } +} diff --git a/index/upsidedown/store/null/null_test.go b/index/upsidedown/store/null/null_test.go new file mode 100644 index 0000000..2042f4d --- /dev/null +++ b/index/upsidedown/store/null/null_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package null + +import ( + "testing" + + store "github.com/blevesearch/upsidedown_store_api" +) + +func TestStore(t *testing.T) { + s, err := New(nil, nil) + if err != nil { + t.Fatal(err) + } + + NullTestKVStore(t, s) +} + +// NullTestKVStore has very different expectations +// compared to CommonTestKVStore +func NullTestKVStore(t *testing.T, s store.KVStore) { + + writer, err := s.Writer() + if err != nil { + t.Error(err) + } + + batch := writer.NewBatch() + batch.Set([]byte("b"), []byte("val-b")) + batch.Set([]byte("c"), []byte("val-c")) + batch.Set([]byte("d"), []byte("val-d")) + batch.Set([]byte("e"), []byte("val-e")) + batch.Set([]byte("f"), []byte("val-f")) + batch.Set([]byte("g"), []byte("val-g")) + batch.Set([]byte("h"), []byte("val-h")) + batch.Set([]byte("i"), []byte("val-i")) + batch.Set([]byte("j"), []byte("val-j")) + + err = writer.ExecuteBatch(batch) + if err != nil { + t.Fatal(err) + } + err = writer.Close() + if err != nil { + t.Fatal(err) + } + + reader, err := s.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := reader.Close() + if err != nil { + t.Fatal(err) + } + }() + it := reader.RangeIterator([]byte("b"), nil) + key, val, valid := it.Current() + if valid { + t.Fatalf("valid true, expected false") + } + if key != nil { + t.Fatalf("expected key nil, got %s", key) + } + if val != nil { + t.Fatalf("expected value nil, got %s", val) + } + + err = it.Close() + if err != nil { + t.Fatal(err) + } + + err = s.Close() + if err != nil { + t.Fatal(err) + } +} diff --git a/index/upsidedown/upsidedown.go b/index/upsidedown/upsidedown.go new file mode 100644 index 0000000..2400776 --- /dev/null +++ b/index/upsidedown/upsidedown.go @@ -0,0 +1,1079 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate protoc --gofast_out=. upsidedown.proto + +package upsidedown + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/registry" + index "github.com/blevesearch/bleve_index_api" + store "github.com/blevesearch/upsidedown_store_api" + + "github.com/golang/protobuf/proto" +) + +const Name = "upside_down" + +// RowBufferSize should ideally this is sized to be the smallest +// size that can contain an index row key and its corresponding +// value. It is not a limit, if need be a larger buffer is +// allocated, but performance will be more optimal if *most* +// rows fit this size. +const RowBufferSize = 4 * 1024 + +var VersionKey = []byte{'v'} + +const Version uint8 = 7 + +var IncompatibleVersion = fmt.Errorf("incompatible version, %d is supported", Version) + +var ErrorUnknownStorageType = fmt.Errorf("unknown storage type") + +type UpsideDownCouch struct { + version uint8 + path string + storeName string + storeConfig map[string]interface{} + store store.KVStore + fieldCache *FieldCache + analysisQueue *index.AnalysisQueue + stats *indexStat + + m sync.RWMutex + // fields protected by m + docCount uint64 + + writeMutex sync.Mutex +} + +type docBackIndexRow struct { + docID string + doc index.Document // If deletion, doc will be nil. + backIndexRow *BackIndexRow +} + +func NewUpsideDownCouch(storeName string, storeConfig map[string]interface{}, analysisQueue *index.AnalysisQueue) (index.Index, error) { + rv := &UpsideDownCouch{ + version: Version, + fieldCache: NewFieldCache(), + storeName: storeName, + storeConfig: storeConfig, + analysisQueue: analysisQueue, + } + rv.stats = &indexStat{i: rv} + return rv, nil +} + +func (udc *UpsideDownCouch) init(kvwriter store.KVWriter) (err error) { + // version marker + rowsAll := [][]UpsideDownCouchRow{ + {NewVersionRow(udc.version)}, + } + + err = udc.batchRows(kvwriter, nil, rowsAll, nil) + return +} + +func (udc *UpsideDownCouch) loadSchema(kvreader store.KVReader) (err error) { + + it := kvreader.PrefixIterator([]byte{'f'}) + defer func() { + if cerr := it.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + key, val, valid := it.Current() + for valid { + var fieldRow *FieldRow + fieldRow, err = NewFieldRowKV(key, val) + if err != nil { + return + } + udc.fieldCache.AddExisting(fieldRow.name, fieldRow.index) + + it.Next() + key, val, valid = it.Current() + } + + val, err = kvreader.Get([]byte{'v'}) + if err != nil { + return + } + var vr *VersionRow + vr, err = NewVersionRowKV([]byte{'v'}, val) + if err != nil { + return + } + if vr.version != Version { + err = IncompatibleVersion + return + } + + return +} + +type rowBuffer struct { + buf []byte +} + +var rowBufferPool sync.Pool + +func GetRowBuffer() *rowBuffer { + if rb, ok := rowBufferPool.Get().(*rowBuffer); ok { + return rb + } else { + buf := make([]byte, RowBufferSize) + return &rowBuffer{buf: buf} + } +} + +func PutRowBuffer(rb *rowBuffer) { + rowBufferPool.Put(rb) +} + +func (udc *UpsideDownCouch) batchRows(writer store.KVWriter, addRowsAll [][]UpsideDownCouchRow, updateRowsAll [][]UpsideDownCouchRow, deleteRowsAll [][]UpsideDownCouchRow) (err error) { + dictionaryDeltas := make(map[string]int64) + + // count up bytes needed for buffering. + addNum := 0 + addKeyBytes := 0 + addValBytes := 0 + + updateNum := 0 + updateKeyBytes := 0 + updateValBytes := 0 + + deleteNum := 0 + deleteKeyBytes := 0 + + rowBuf := GetRowBuffer() + + for _, addRows := range addRowsAll { + for _, row := range addRows { + tfr, ok := row.(*TermFrequencyRow) + if ok { + if tfr.DictionaryRowKeySize() > len(rowBuf.buf) { + rowBuf.buf = make([]byte, tfr.DictionaryRowKeySize()) + } + dictKeySize, err := tfr.DictionaryRowKeyTo(rowBuf.buf) + if err != nil { + return err + } + dictionaryDeltas[string(rowBuf.buf[:dictKeySize])] += 1 + } + addKeyBytes += row.KeySize() + addValBytes += row.ValueSize() + } + addNum += len(addRows) + } + + for _, updateRows := range updateRowsAll { + for _, row := range updateRows { + updateKeyBytes += row.KeySize() + updateValBytes += row.ValueSize() + } + updateNum += len(updateRows) + } + + for _, deleteRows := range deleteRowsAll { + for _, row := range deleteRows { + tfr, ok := row.(*TermFrequencyRow) + if ok { + // need to decrement counter + if tfr.DictionaryRowKeySize() > len(rowBuf.buf) { + rowBuf.buf = make([]byte, tfr.DictionaryRowKeySize()) + } + dictKeySize, err := tfr.DictionaryRowKeyTo(rowBuf.buf) + if err != nil { + return err + } + dictionaryDeltas[string(rowBuf.buf[:dictKeySize])] -= 1 + } + deleteKeyBytes += row.KeySize() + } + deleteNum += len(deleteRows) + } + + PutRowBuffer(rowBuf) + + mergeNum := len(dictionaryDeltas) + mergeKeyBytes := 0 + mergeValBytes := mergeNum * DictionaryRowMaxValueSize + + for dictRowKey := range dictionaryDeltas { + mergeKeyBytes += len(dictRowKey) + } + + // prepare batch + totBytes := addKeyBytes + addValBytes + + updateKeyBytes + updateValBytes + + deleteKeyBytes + + 2*(mergeKeyBytes+mergeValBytes) + + buf, wb, err := writer.NewBatchEx(store.KVBatchOptions{ + TotalBytes: totBytes, + NumSets: addNum + updateNum, + NumDeletes: deleteNum, + NumMerges: mergeNum, + }) + if err != nil { + return err + } + defer func() { + _ = wb.Close() + }() + + // fill the batch + for _, addRows := range addRowsAll { + for _, row := range addRows { + keySize, err := row.KeyTo(buf) + if err != nil { + return err + } + valSize, err := row.ValueTo(buf[keySize:]) + if err != nil { + return err + } + wb.Set(buf[:keySize], buf[keySize:keySize+valSize]) + buf = buf[keySize+valSize:] + } + } + + for _, updateRows := range updateRowsAll { + for _, row := range updateRows { + keySize, err := row.KeyTo(buf) + if err != nil { + return err + } + valSize, err := row.ValueTo(buf[keySize:]) + if err != nil { + return err + } + wb.Set(buf[:keySize], buf[keySize:keySize+valSize]) + buf = buf[keySize+valSize:] + } + } + + for _, deleteRows := range deleteRowsAll { + for _, row := range deleteRows { + keySize, err := row.KeyTo(buf) + if err != nil { + return err + } + wb.Delete(buf[:keySize]) + buf = buf[keySize:] + } + } + + for dictRowKey, delta := range dictionaryDeltas { + dictRowKeyLen := copy(buf, dictRowKey) + binary.LittleEndian.PutUint64(buf[dictRowKeyLen:], uint64(delta)) + wb.Merge(buf[:dictRowKeyLen], buf[dictRowKeyLen:dictRowKeyLen+DictionaryRowMaxValueSize]) + buf = buf[dictRowKeyLen+DictionaryRowMaxValueSize:] + } + + // write out the batch + return writer.ExecuteBatch(wb) +} + +func (udc *UpsideDownCouch) Open() (err error) { + // acquire the write mutex for the duration of Open() + udc.writeMutex.Lock() + defer udc.writeMutex.Unlock() + + // open the kv store + storeConstructor := registry.KVStoreConstructorByName(udc.storeName) + if storeConstructor == nil { + err = ErrorUnknownStorageType + return + } + + // now open the store + udc.store, err = storeConstructor(&mergeOperator, udc.storeConfig) + if err != nil { + return + } + + // start a reader to look at the index + var kvreader store.KVReader + kvreader, err = udc.store.Reader() + if err != nil { + return + } + + var value []byte + value, err = kvreader.Get(VersionKey) + if err != nil { + _ = kvreader.Close() + return + } + + if value != nil { + err = udc.loadSchema(kvreader) + if err != nil { + _ = kvreader.Close() + return + } + + // set doc count + udc.m.Lock() + udc.docCount, err = udc.countDocs(kvreader) + udc.m.Unlock() + + err = kvreader.Close() + } else { + // new index, close the reader and open writer to init + err = kvreader.Close() + if err != nil { + return + } + + var kvwriter store.KVWriter + kvwriter, err = udc.store.Writer() + if err != nil { + return + } + defer func() { + if cerr := kvwriter.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + // init the index + err = udc.init(kvwriter) + } + + return +} + +func (udc *UpsideDownCouch) countDocs(kvreader store.KVReader) (count uint64, err error) { + it := kvreader.PrefixIterator([]byte{'b'}) + defer func() { + if cerr := it.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + _, _, valid := it.Current() + for valid { + count++ + it.Next() + _, _, valid = it.Current() + } + + return +} + +func (udc *UpsideDownCouch) rowCount() (count uint64, err error) { + // start an isolated reader for use during the rowcount + kvreader, err := udc.store.Reader() + if err != nil { + return + } + defer func() { + if cerr := kvreader.Close(); err == nil && cerr != nil { + err = cerr + } + }() + it := kvreader.RangeIterator(nil, nil) + defer func() { + if cerr := it.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + _, _, valid := it.Current() + for valid { + count++ + it.Next() + _, _, valid = it.Current() + } + + return +} + +func (udc *UpsideDownCouch) Close() error { + return udc.store.Close() +} + +func (udc *UpsideDownCouch) Update(doc index.Document) (err error) { + // do analysis before acquiring write lock + analysisStart := time.Now() + resultChan := make(chan *AnalysisResult) + + // put the work on the queue + udc.analysisQueue.Queue(func() { + ar := udc.analyze(doc) + resultChan <- ar + }) + + // wait for the result + result := <-resultChan + close(resultChan) + atomic.AddUint64(&udc.stats.analysisTime, uint64(time.Since(analysisStart))) + + udc.writeMutex.Lock() + defer udc.writeMutex.Unlock() + + // open a reader for backindex lookup + var kvreader store.KVReader + kvreader, err = udc.store.Reader() + if err != nil { + return + } + + // first we lookup the backindex row for the doc id if it exists + // lookup the back index row + var backIndexRow *BackIndexRow + backIndexRow, err = backIndexRowForDoc(kvreader, index.IndexInternalID(doc.ID())) + if err != nil { + _ = kvreader.Close() + atomic.AddUint64(&udc.stats.errors, 1) + return + } + + err = kvreader.Close() + if err != nil { + return + } + + return udc.UpdateWithAnalysis(doc, result, backIndexRow) +} + +func (udc *UpsideDownCouch) UpdateWithAnalysis(doc index.Document, + result *AnalysisResult, backIndexRow *BackIndexRow) (err error) { + // start a writer for this update + indexStart := time.Now() + var kvwriter store.KVWriter + kvwriter, err = udc.store.Writer() + if err != nil { + return + } + defer func() { + if cerr := kvwriter.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + // prepare a list of rows + var addRowsAll [][]UpsideDownCouchRow + var updateRowsAll [][]UpsideDownCouchRow + var deleteRowsAll [][]UpsideDownCouchRow + + addRows, updateRows, deleteRows := udc.mergeOldAndNew(backIndexRow, result.Rows) + if len(addRows) > 0 { + addRowsAll = append(addRowsAll, addRows) + } + if len(updateRows) > 0 { + updateRowsAll = append(updateRowsAll, updateRows) + } + if len(deleteRows) > 0 { + deleteRowsAll = append(deleteRowsAll, deleteRows) + } + + err = udc.batchRows(kvwriter, addRowsAll, updateRowsAll, deleteRowsAll) + if err == nil && backIndexRow == nil { + udc.m.Lock() + udc.docCount++ + udc.m.Unlock() + } + atomic.AddUint64(&udc.stats.indexTime, uint64(time.Since(indexStart))) + if err == nil { + atomic.AddUint64(&udc.stats.updates, 1) + atomic.AddUint64(&udc.stats.numPlainTextBytesIndexed, doc.NumPlainTextBytes()) + } else { + atomic.AddUint64(&udc.stats.errors, 1) + } + return +} + +func (udc *UpsideDownCouch) mergeOldAndNew(backIndexRow *BackIndexRow, rows []IndexRow) (addRows []UpsideDownCouchRow, updateRows []UpsideDownCouchRow, deleteRows []UpsideDownCouchRow) { + addRows = make([]UpsideDownCouchRow, 0, len(rows)) + + if backIndexRow == nil { + addRows = addRows[0:len(rows)] + for i, row := range rows { + addRows[i] = row + } + return addRows, nil, nil + } + + updateRows = make([]UpsideDownCouchRow, 0, len(rows)) + deleteRows = make([]UpsideDownCouchRow, 0, len(rows)) + + var existingTermKeys map[string]struct{} + backIndexTermKeys := backIndexRow.AllTermKeys() + if len(backIndexTermKeys) > 0 { + existingTermKeys = make(map[string]struct{}, len(backIndexTermKeys)) + for _, key := range backIndexTermKeys { + existingTermKeys[string(key)] = struct{}{} + } + } + + var existingStoredKeys map[string]struct{} + backIndexStoredKeys := backIndexRow.AllStoredKeys() + if len(backIndexStoredKeys) > 0 { + existingStoredKeys = make(map[string]struct{}, len(backIndexStoredKeys)) + for _, key := range backIndexStoredKeys { + existingStoredKeys[string(key)] = struct{}{} + } + } + + keyBuf := GetRowBuffer() + for _, row := range rows { + switch row := row.(type) { + case *TermFrequencyRow: + if existingTermKeys != nil { + if row.KeySize() > len(keyBuf.buf) { + keyBuf.buf = make([]byte, row.KeySize()) + } + keySize, _ := row.KeyTo(keyBuf.buf) + if _, ok := existingTermKeys[string(keyBuf.buf[:keySize])]; ok { + updateRows = append(updateRows, row) + delete(existingTermKeys, string(keyBuf.buf[:keySize])) + continue + } + } + addRows = append(addRows, row) + case *StoredRow: + if existingStoredKeys != nil { + if row.KeySize() > len(keyBuf.buf) { + keyBuf.buf = make([]byte, row.KeySize()) + } + keySize, _ := row.KeyTo(keyBuf.buf) + if _, ok := existingStoredKeys[string(keyBuf.buf[:keySize])]; ok { + updateRows = append(updateRows, row) + delete(existingStoredKeys, string(keyBuf.buf[:keySize])) + continue + } + } + addRows = append(addRows, row) + default: + updateRows = append(updateRows, row) + } + } + PutRowBuffer(keyBuf) + + // any of the existing rows that weren't updated need to be deleted + for existingTermKey := range existingTermKeys { + termFreqRow, err := NewTermFrequencyRowK([]byte(existingTermKey)) + if err == nil { + deleteRows = append(deleteRows, termFreqRow) + } + } + + // any of the existing stored fields that weren't updated need to be deleted + for existingStoredKey := range existingStoredKeys { + storedRow, err := NewStoredRowK([]byte(existingStoredKey)) + if err == nil { + deleteRows = append(deleteRows, storedRow) + } + } + + return addRows, updateRows, deleteRows +} + +func (udc *UpsideDownCouch) storeField(docID []byte, field index.Field, fieldIndex uint16, rows []IndexRow, backIndexStoredEntries []*BackIndexStoreEntry) ([]IndexRow, []*BackIndexStoreEntry) { + fieldType := field.EncodedFieldType() + storedRow := NewStoredRow(docID, fieldIndex, field.ArrayPositions(), fieldType, field.Value()) + + // record the back index entry + backIndexStoredEntry := BackIndexStoreEntry{Field: proto.Uint32(uint32(fieldIndex)), ArrayPositions: field.ArrayPositions()} + + return append(rows, storedRow), append(backIndexStoredEntries, &backIndexStoredEntry) +} + +func (udc *UpsideDownCouch) indexField(docID []byte, includeTermVectors bool, fieldIndex uint16, fieldLength int, tokenFreqs index.TokenFrequencies, rows []IndexRow, backIndexTermsEntries []*BackIndexTermsEntry) ([]IndexRow, []*BackIndexTermsEntry) { + fieldNorm := float32(1.0 / math.Sqrt(float64(fieldLength))) + + termFreqRows := make([]TermFrequencyRow, len(tokenFreqs)) + termFreqRowsUsed := 0 + + terms := make([]string, 0, len(tokenFreqs)) + for k, tf := range tokenFreqs { + termFreqRow := &termFreqRows[termFreqRowsUsed] + termFreqRowsUsed++ + + InitTermFrequencyRow(termFreqRow, tf.Term, fieldIndex, docID, + uint64(frequencyFromTokenFreq(tf)), fieldNorm) + + if includeTermVectors { + termFreqRow.vectors, rows = udc.termVectorsFromTokenFreq(fieldIndex, tf, rows) + } + + // record the back index entry + terms = append(terms, k) + + rows = append(rows, termFreqRow) + } + backIndexTermsEntry := BackIndexTermsEntry{Field: proto.Uint32(uint32(fieldIndex)), Terms: terms} + backIndexTermsEntries = append(backIndexTermsEntries, &backIndexTermsEntry) + + return rows, backIndexTermsEntries +} + +func (udc *UpsideDownCouch) Delete(id string) (err error) { + indexStart := time.Now() + + udc.writeMutex.Lock() + defer udc.writeMutex.Unlock() + + // open a reader for backindex lookup + var kvreader store.KVReader + kvreader, err = udc.store.Reader() + if err != nil { + return + } + + // first we lookup the backindex row for the doc id if it exists + // lookup the back index row + var backIndexRow *BackIndexRow + backIndexRow, err = backIndexRowForDoc(kvreader, index.IndexInternalID(id)) + if err != nil { + _ = kvreader.Close() + atomic.AddUint64(&udc.stats.errors, 1) + return + } + + err = kvreader.Close() + if err != nil { + return + } + + if backIndexRow == nil { + atomic.AddUint64(&udc.stats.deletes, 1) + return + } + + // start a writer for this delete + var kvwriter store.KVWriter + kvwriter, err = udc.store.Writer() + if err != nil { + return + } + defer func() { + if cerr := kvwriter.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + var deleteRowsAll [][]UpsideDownCouchRow + + deleteRows := udc.deleteSingle(id, backIndexRow, nil) + if len(deleteRows) > 0 { + deleteRowsAll = append(deleteRowsAll, deleteRows) + } + + err = udc.batchRows(kvwriter, nil, nil, deleteRowsAll) + if err == nil { + udc.m.Lock() + udc.docCount-- + udc.m.Unlock() + } + atomic.AddUint64(&udc.stats.indexTime, uint64(time.Since(indexStart))) + if err == nil { + atomic.AddUint64(&udc.stats.deletes, 1) + } else { + atomic.AddUint64(&udc.stats.errors, 1) + } + return +} + +func (udc *UpsideDownCouch) deleteSingle(id string, backIndexRow *BackIndexRow, deleteRows []UpsideDownCouchRow) []UpsideDownCouchRow { + idBytes := []byte(id) + + for _, backIndexEntry := range backIndexRow.termsEntries { + for i := range backIndexEntry.Terms { + tfr := NewTermFrequencyRow([]byte(backIndexEntry.Terms[i]), uint16(*backIndexEntry.Field), idBytes, 0, 0) + deleteRows = append(deleteRows, tfr) + } + } + for _, se := range backIndexRow.storedEntries { + sf := NewStoredRow(idBytes, uint16(*se.Field), se.ArrayPositions, 'x', nil) + deleteRows = append(deleteRows, sf) + } + + // also delete the back entry itself + deleteRows = append(deleteRows, backIndexRow) + return deleteRows +} + +func decodeFieldType(typ byte, name string, pos []uint64, value []byte) document.Field { + switch typ { + case 't': + return document.NewTextField(name, pos, value) + case 'n': + return document.NewNumericFieldFromBytes(name, pos, value) + case 'd': + return document.NewDateTimeFieldFromBytes(name, pos, value) + case 'b': + return document.NewBooleanFieldFromBytes(name, pos, value) + case 'g': + return document.NewGeoPointFieldFromBytes(name, pos, value) + case 'i': + return document.NewIPFieldFromBytes(name, pos, value) + } + return nil +} + +func frequencyFromTokenFreq(tf *index.TokenFreq) int { + return tf.Frequency() +} + +func (udc *UpsideDownCouch) termVectorsFromTokenFreq(field uint16, tf *index.TokenFreq, rows []IndexRow) ([]*TermVector, []IndexRow) { + a := make([]TermVector, len(tf.Locations)) + rv := make([]*TermVector, len(tf.Locations)) + + for i, l := range tf.Locations { + var newFieldRow *FieldRow + fieldIndex := field + if l.Field != "" { + // lookup correct field + fieldIndex, newFieldRow = udc.fieldIndexOrNewRow(l.Field) + if newFieldRow != nil { + rows = append(rows, newFieldRow) + } + } + a[i] = TermVector{ + field: fieldIndex, + arrayPositions: l.ArrayPositions, + pos: uint64(l.Position), + start: uint64(l.Start), + end: uint64(l.End), + } + rv[i] = &a[i] + } + + return rv, rows +} + +func (udc *UpsideDownCouch) termFieldVectorsFromTermVectors(in []*TermVector) []*index.TermFieldVector { + if len(in) == 0 { + return nil + } + + a := make([]index.TermFieldVector, len(in)) + rv := make([]*index.TermFieldVector, len(in)) + + for i, tv := range in { + fieldName := udc.fieldCache.FieldIndexed(tv.field) + a[i] = index.TermFieldVector{ + Field: fieldName, + ArrayPositions: tv.arrayPositions, + Pos: tv.pos, + Start: tv.start, + End: tv.end, + } + rv[i] = &a[i] + } + return rv +} + +func (udc *UpsideDownCouch) Batch(batch *index.Batch) (err error) { + persistedCallback := batch.PersistedCallback() + if persistedCallback != nil { + defer persistedCallback(err) + } + analysisStart := time.Now() + + resultChan := make(chan *AnalysisResult, len(batch.IndexOps)) + + var numUpdates uint64 + var numPlainTextBytes uint64 + for _, doc := range batch.IndexOps { + if doc != nil { + numUpdates++ + numPlainTextBytes += doc.NumPlainTextBytes() + } + } + + if numUpdates > 0 { + go func() { + for k := range batch.IndexOps { + doc := batch.IndexOps[k] + if doc != nil { + // put the work on the queue + udc.analysisQueue.Queue(func() { + ar := udc.analyze(doc) + resultChan <- ar + }) + } + } + }() + } + + // retrieve back index rows concurrent with analysis + docBackIndexRowErr := error(nil) + docBackIndexRowCh := make(chan *docBackIndexRow, len(batch.IndexOps)) + + udc.writeMutex.Lock() + defer udc.writeMutex.Unlock() + + go func() { + defer close(docBackIndexRowCh) + + // open a reader for backindex lookup + var kvreader store.KVReader + kvreader, err = udc.store.Reader() + if err != nil { + docBackIndexRowErr = err + return + } + defer func() { + if cerr := kvreader.Close(); err == nil && cerr != nil { + docBackIndexRowErr = cerr + } + }() + + for docID, doc := range batch.IndexOps { + backIndexRow, err := backIndexRowForDoc(kvreader, index.IndexInternalID(docID)) + if err != nil { + docBackIndexRowErr = err + return + } + + docBackIndexRowCh <- &docBackIndexRow{docID, doc, backIndexRow} + } + }() + + // wait for analysis result + newRowsMap := make(map[string][]IndexRow) + var itemsDeQueued uint64 + for itemsDeQueued < numUpdates { + result := <-resultChan + newRowsMap[result.DocID] = result.Rows + itemsDeQueued++ + } + close(resultChan) + + atomic.AddUint64(&udc.stats.analysisTime, uint64(time.Since(analysisStart))) + + docsAdded := uint64(0) + docsDeleted := uint64(0) + + indexStart := time.Now() + + // prepare a list of rows + var addRowsAll [][]UpsideDownCouchRow + var updateRowsAll [][]UpsideDownCouchRow + var deleteRowsAll [][]UpsideDownCouchRow + + // add the internal ops + var updateRows []UpsideDownCouchRow + var deleteRows []UpsideDownCouchRow + + for internalKey, internalValue := range batch.InternalOps { + if internalValue == nil { + // delete + deleteInternalRow := NewInternalRow([]byte(internalKey), nil) + deleteRows = append(deleteRows, deleteInternalRow) + } else { + updateInternalRow := NewInternalRow([]byte(internalKey), internalValue) + updateRows = append(updateRows, updateInternalRow) + } + } + + if len(updateRows) > 0 { + updateRowsAll = append(updateRowsAll, updateRows) + } + if len(deleteRows) > 0 { + deleteRowsAll = append(deleteRowsAll, deleteRows) + } + + // process back index rows as they arrive + for dbir := range docBackIndexRowCh { + if dbir.doc == nil && dbir.backIndexRow != nil { + // delete + deleteRows := udc.deleteSingle(dbir.docID, dbir.backIndexRow, nil) + if len(deleteRows) > 0 { + deleteRowsAll = append(deleteRowsAll, deleteRows) + } + docsDeleted++ + } else if dbir.doc != nil { + addRows, updateRows, deleteRows := udc.mergeOldAndNew(dbir.backIndexRow, newRowsMap[dbir.docID]) + if len(addRows) > 0 { + addRowsAll = append(addRowsAll, addRows) + } + if len(updateRows) > 0 { + updateRowsAll = append(updateRowsAll, updateRows) + } + if len(deleteRows) > 0 { + deleteRowsAll = append(deleteRowsAll, deleteRows) + } + if dbir.backIndexRow == nil { + docsAdded++ + } + } + } + + if docBackIndexRowErr != nil { + return docBackIndexRowErr + } + + // start a writer for this batch + var kvwriter store.KVWriter + kvwriter, err = udc.store.Writer() + if err != nil { + return + } + + err = udc.batchRows(kvwriter, addRowsAll, updateRowsAll, deleteRowsAll) + if err != nil { + _ = kvwriter.Close() + atomic.AddUint64(&udc.stats.errors, 1) + return + } + + err = kvwriter.Close() + + atomic.AddUint64(&udc.stats.indexTime, uint64(time.Since(indexStart))) + + if err == nil { + udc.m.Lock() + udc.docCount += docsAdded + udc.docCount -= docsDeleted + udc.m.Unlock() + atomic.AddUint64(&udc.stats.updates, numUpdates) + atomic.AddUint64(&udc.stats.deletes, docsDeleted) + atomic.AddUint64(&udc.stats.batches, 1) + atomic.AddUint64(&udc.stats.numPlainTextBytesIndexed, numPlainTextBytes) + } else { + atomic.AddUint64(&udc.stats.errors, 1) + } + + return +} + +func (udc *UpsideDownCouch) SetInternal(key, val []byte) (err error) { + internalRow := NewInternalRow(key, val) + udc.writeMutex.Lock() + defer udc.writeMutex.Unlock() + var writer store.KVWriter + writer, err = udc.store.Writer() + if err != nil { + return + } + defer func() { + if cerr := writer.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + batch := writer.NewBatch() + batch.Set(internalRow.Key(), internalRow.Value()) + + return writer.ExecuteBatch(batch) +} + +func (udc *UpsideDownCouch) DeleteInternal(key []byte) (err error) { + internalRow := NewInternalRow(key, nil) + udc.writeMutex.Lock() + defer udc.writeMutex.Unlock() + var writer store.KVWriter + writer, err = udc.store.Writer() + if err != nil { + return + } + defer func() { + if cerr := writer.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + batch := writer.NewBatch() + batch.Delete(internalRow.Key()) + return writer.ExecuteBatch(batch) +} + +func (udc *UpsideDownCouch) Reader() (index.IndexReader, error) { + kvr, err := udc.store.Reader() + if err != nil { + return nil, fmt.Errorf("error opening store reader: %v", err) + } + udc.m.RLock() + defer udc.m.RUnlock() + return &IndexReader{ + index: udc, + kvreader: kvr, + docCount: udc.docCount, + }, nil +} + +func (udc *UpsideDownCouch) Stats() json.Marshaler { + return udc.stats +} + +func (udc *UpsideDownCouch) StatsMap() map[string]interface{} { + return udc.stats.statsMap() +} + +func (udc *UpsideDownCouch) Advanced() (store.KVStore, error) { + return udc.store, nil +} + +func (udc *UpsideDownCouch) fieldIndexOrNewRow(name string) (uint16, *FieldRow) { + index, existed := udc.fieldCache.FieldNamed(name, true) + if !existed { + return index, NewFieldRow(index, name) + } + return index, nil +} + +func init() { + err := registry.RegisterIndexType(Name, NewUpsideDownCouch) + if err != nil { + panic(err) + } +} + +func backIndexRowForDoc(kvreader store.KVReader, docID index.IndexInternalID) (*BackIndexRow, error) { + // use a temporary row structure to build key + tempRow := BackIndexRow{ + doc: docID, + } + + keyBuf := GetRowBuffer() + if tempRow.KeySize() > len(keyBuf.buf) { + keyBuf.buf = make([]byte, 2*tempRow.KeySize()) + } + defer PutRowBuffer(keyBuf) + keySize, err := tempRow.KeyTo(keyBuf.buf) + if err != nil { + return nil, err + } + + value, err := kvreader.Get(keyBuf.buf[:keySize]) + if err != nil { + return nil, err + } + if value == nil { + return nil, nil + } + backIndexRow, err := NewBackIndexRowKV(keyBuf.buf[:keySize], value) + if err != nil { + return nil, err + } + return backIndexRow, nil +} diff --git a/index/upsidedown/upsidedown.pb.go b/index/upsidedown/upsidedown.pb.go new file mode 100644 index 0000000..f2080c9 --- /dev/null +++ b/index/upsidedown/upsidedown.pb.go @@ -0,0 +1,690 @@ +// Code generated by protoc-gen-gogo. +// source: upsidedown.proto +// DO NOT EDIT! + +/* +Package upsidedown is a generated protocol buffer package. + +It is generated from these files: + + upsidedown.proto + +It has these top-level messages: + + BackIndexTermsEntry + BackIndexStoreEntry + BackIndexRowValue +*/ +package upsidedown + +import proto "github.com/golang/protobuf/proto" +import math "math" + +import io "io" +import fmt "fmt" +import github_com_golang_protobuf_proto "github.com/golang/protobuf/proto" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = math.Inf + +type BackIndexTermsEntry struct { + Field *uint32 `protobuf:"varint,1,req,name=field" json:"field,omitempty"` + Terms []string `protobuf:"bytes,2,rep,name=terms" json:"terms,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *BackIndexTermsEntry) Reset() { *m = BackIndexTermsEntry{} } +func (m *BackIndexTermsEntry) String() string { return proto.CompactTextString(m) } +func (*BackIndexTermsEntry) ProtoMessage() {} + +func (m *BackIndexTermsEntry) GetField() uint32 { + if m != nil && m.Field != nil { + return *m.Field + } + return 0 +} + +func (m *BackIndexTermsEntry) GetTerms() []string { + if m != nil { + return m.Terms + } + return nil +} + +type BackIndexStoreEntry struct { + Field *uint32 `protobuf:"varint,1,req,name=field" json:"field,omitempty"` + ArrayPositions []uint64 `protobuf:"varint,2,rep,name=arrayPositions" json:"arrayPositions,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *BackIndexStoreEntry) Reset() { *m = BackIndexStoreEntry{} } +func (m *BackIndexStoreEntry) String() string { return proto.CompactTextString(m) } +func (*BackIndexStoreEntry) ProtoMessage() {} + +func (m *BackIndexStoreEntry) GetField() uint32 { + if m != nil && m.Field != nil { + return *m.Field + } + return 0 +} + +func (m *BackIndexStoreEntry) GetArrayPositions() []uint64 { + if m != nil { + return m.ArrayPositions + } + return nil +} + +type BackIndexRowValue struct { + TermsEntries []*BackIndexTermsEntry `protobuf:"bytes,1,rep,name=termsEntries" json:"termsEntries,omitempty"` + StoredEntries []*BackIndexStoreEntry `protobuf:"bytes,2,rep,name=storedEntries" json:"storedEntries,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *BackIndexRowValue) Reset() { *m = BackIndexRowValue{} } +func (m *BackIndexRowValue) String() string { return proto.CompactTextString(m) } +func (*BackIndexRowValue) ProtoMessage() {} + +func (m *BackIndexRowValue) GetTermsEntries() []*BackIndexTermsEntry { + if m != nil { + return m.TermsEntries + } + return nil +} + +func (m *BackIndexRowValue) GetStoredEntries() []*BackIndexStoreEntry { + if m != nil { + return m.StoredEntries + } + return nil +} + +func (m *BackIndexTermsEntry) Unmarshal(data []byte) error { + var hasFields [1]uint64 + l := len(data) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Field", wireType) + } + var v uint32 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + v |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.Field = &v + hasFields[0] |= uint64(0x00000001) + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Terms", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + postIndex := iNdEx + int(stringLen) + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Terms = append(m.Terms, string(data[iNdEx:postIndex])) + iNdEx = postIndex + default: + var sizeOfWire int + for { + sizeOfWire++ + wire >>= 7 + if wire == 0 { + break + } + } + iNdEx -= sizeOfWire + skippy, err := skipUpsidedown(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthUpsidedown + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + if hasFields[0]&uint64(0x00000001) == 0 { + return new(github_com_golang_protobuf_proto.RequiredNotSetError) + } + + return nil +} +func (m *BackIndexStoreEntry) Unmarshal(data []byte) error { + var hasFields [1]uint64 + l := len(data) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Field", wireType) + } + var v uint32 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + v |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.Field = &v + hasFields[0] |= uint64(0x00000001) + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ArrayPositions", wireType) + } + var v uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + v |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.ArrayPositions = append(m.ArrayPositions, v) + default: + var sizeOfWire int + for { + sizeOfWire++ + wire >>= 7 + if wire == 0 { + break + } + } + iNdEx -= sizeOfWire + skippy, err := skipUpsidedown(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthUpsidedown + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + if hasFields[0]&uint64(0x00000001) == 0 { + return new(github_com_golang_protobuf_proto.RequiredNotSetError) + } + + return nil +} +func (m *BackIndexRowValue) Unmarshal(data []byte) error { + l := len(data) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TermsEntries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + postIndex := iNdEx + msglen + if msglen < 0 { + return ErrInvalidLengthUpsidedown + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.TermsEntries = append(m.TermsEntries, &BackIndexTermsEntry{}) + if err := m.TermsEntries[len(m.TermsEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StoredEntries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + postIndex := iNdEx + msglen + if msglen < 0 { + return ErrInvalidLengthUpsidedown + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.StoredEntries = append(m.StoredEntries, &BackIndexStoreEntry{}) + if err := m.StoredEntries[len(m.StoredEntries)-1].Unmarshal(data[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + var sizeOfWire int + for { + sizeOfWire++ + wire >>= 7 + if wire == 0 { + break + } + } + iNdEx -= sizeOfWire + skippy, err := skipUpsidedown(data[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthUpsidedown + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, data[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + return nil +} +func skipUpsidedown(data []byte) (n int, err error) { + l := len(data) + iNdEx := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for { + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if data[iNdEx-1] < 0x80 { + break + } + } + return iNdEx, nil + case 1: + iNdEx += 8 + return iNdEx, nil + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + iNdEx += length + if length < 0 { + return 0, ErrInvalidLengthUpsidedown + } + return iNdEx, nil + case 3: + for { + var innerWire uint64 + var start int = iNdEx + for shift := uint(0); ; shift += 7 { + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := data[iNdEx] + iNdEx++ + innerWire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + innerWireType := int(innerWire & 0x7) + if innerWireType == 4 { + break + } + next, err := skipUpsidedown(data[start:]) + if err != nil { + return 0, err + } + iNdEx = start + next + } + return iNdEx, nil + case 4: + return iNdEx, nil + case 5: + iNdEx += 4 + return iNdEx, nil + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + } + panic("unreachable") +} + +var ( + ErrInvalidLengthUpsidedown = fmt.Errorf("proto: negative length found during unmarshaling") +) + +func (m *BackIndexTermsEntry) Size() (n int) { + var l int + _ = l + if m.Field != nil { + n += 1 + sovUpsidedown(uint64(*m.Field)) + } + if len(m.Terms) > 0 { + for _, s := range m.Terms { + l = len(s) + n += 1 + l + sovUpsidedown(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *BackIndexStoreEntry) Size() (n int) { + var l int + _ = l + if m.Field != nil { + n += 1 + sovUpsidedown(uint64(*m.Field)) + } + if len(m.ArrayPositions) > 0 { + for _, e := range m.ArrayPositions { + n += 1 + sovUpsidedown(uint64(e)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *BackIndexRowValue) Size() (n int) { + var l int + _ = l + if len(m.TermsEntries) > 0 { + for _, e := range m.TermsEntries { + l = e.Size() + n += 1 + l + sovUpsidedown(uint64(l)) + } + } + if len(m.StoredEntries) > 0 { + for _, e := range m.StoredEntries { + l = e.Size() + n += 1 + l + sovUpsidedown(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func sovUpsidedown(x uint64) (n int) { + for { + n++ + x >>= 7 + if x == 0 { + break + } + } + return n +} +func sozUpsidedown(x uint64) (n int) { + return sovUpsidedown(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *BackIndexTermsEntry) Marshal() (data []byte, err error) { + size := m.Size() + data = make([]byte, size) + n, err := m.MarshalTo(data) + if err != nil { + return nil, err + } + return data[:n], nil +} + +func (m *BackIndexTermsEntry) MarshalTo(data []byte) (n int, err error) { + var i int + _ = i + var l int + _ = l + if m.Field == nil { + return 0, new(github_com_golang_protobuf_proto.RequiredNotSetError) + } else { + data[i] = 0x8 + i++ + i = encodeVarintUpsidedown(data, i, uint64(*m.Field)) + } + if len(m.Terms) > 0 { + for _, s := range m.Terms { + data[i] = 0x12 + i++ + l = len(s) + for l >= 1<<7 { + data[i] = uint8(uint64(l)&0x7f | 0x80) + l >>= 7 + i++ + } + data[i] = uint8(l) + i++ + i += copy(data[i:], s) + } + } + if m.XXX_unrecognized != nil { + i += copy(data[i:], m.XXX_unrecognized) + } + return i, nil +} + +func (m *BackIndexStoreEntry) Marshal() (data []byte, err error) { + size := m.Size() + data = make([]byte, size) + n, err := m.MarshalTo(data) + if err != nil { + return nil, err + } + return data[:n], nil +} + +func (m *BackIndexStoreEntry) MarshalTo(data []byte) (n int, err error) { + var i int + _ = i + var l int + _ = l + if m.Field == nil { + return 0, new(github_com_golang_protobuf_proto.RequiredNotSetError) + } else { + data[i] = 0x8 + i++ + i = encodeVarintUpsidedown(data, i, uint64(*m.Field)) + } + if len(m.ArrayPositions) > 0 { + for _, num := range m.ArrayPositions { + data[i] = 0x10 + i++ + i = encodeVarintUpsidedown(data, i, uint64(num)) + } + } + if m.XXX_unrecognized != nil { + i += copy(data[i:], m.XXX_unrecognized) + } + return i, nil +} + +func (m *BackIndexRowValue) Marshal() (data []byte, err error) { + size := m.Size() + data = make([]byte, size) + n, err := m.MarshalTo(data) + if err != nil { + return nil, err + } + return data[:n], nil +} + +func (m *BackIndexRowValue) MarshalTo(data []byte) (n int, err error) { + var i int + _ = i + var l int + _ = l + if len(m.TermsEntries) > 0 { + for _, msg := range m.TermsEntries { + data[i] = 0xa + i++ + i = encodeVarintUpsidedown(data, i, uint64(msg.Size())) + n, err := msg.MarshalTo(data[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if len(m.StoredEntries) > 0 { + for _, msg := range m.StoredEntries { + data[i] = 0x12 + i++ + i = encodeVarintUpsidedown(data, i, uint64(msg.Size())) + n, err := msg.MarshalTo(data[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.XXX_unrecognized != nil { + i += copy(data[i:], m.XXX_unrecognized) + } + return i, nil +} + +func encodeFixed64Upsidedown(data []byte, offset int, v uint64) int { + data[offset] = uint8(v) + data[offset+1] = uint8(v >> 8) + data[offset+2] = uint8(v >> 16) + data[offset+3] = uint8(v >> 24) + data[offset+4] = uint8(v >> 32) + data[offset+5] = uint8(v >> 40) + data[offset+6] = uint8(v >> 48) + data[offset+7] = uint8(v >> 56) + return offset + 8 +} +func encodeFixed32Upsidedown(data []byte, offset int, v uint32) int { + data[offset] = uint8(v) + data[offset+1] = uint8(v >> 8) + data[offset+2] = uint8(v >> 16) + data[offset+3] = uint8(v >> 24) + return offset + 4 +} +func encodeVarintUpsidedown(data []byte, offset int, v uint64) int { + for v >= 1<<7 { + data[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + data[offset] = uint8(v) + return offset + 1 +} diff --git a/index/upsidedown/upsidedown.proto b/index/upsidedown/upsidedown.proto new file mode 100644 index 0000000..cf0492a --- /dev/null +++ b/index/upsidedown/upsidedown.proto @@ -0,0 +1,14 @@ +message BackIndexTermsEntry { + required uint32 field = 1; + repeated string terms = 2; +} + +message BackIndexStoreEntry { + required uint32 field = 1; + repeated uint64 arrayPositions = 2; +} + +message BackIndexRowValue { + repeated BackIndexTermsEntry termsEntries = 1; + repeated BackIndexStoreEntry storedEntries = 2; +} diff --git a/index/upsidedown/upsidedown_test.go b/index/upsidedown/upsidedown_test.go new file mode 100644 index 0000000..36ffed0 --- /dev/null +++ b/index/upsidedown/upsidedown_test.go @@ -0,0 +1,1529 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package upsidedown + +import ( + "context" + "log" + "reflect" + "regexp" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + regexpTokenizer "github.com/blevesearch/bleve/v2/analysis/tokenizer/regexp" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/null" + "github.com/blevesearch/bleve/v2/registry" + index "github.com/blevesearch/bleve_index_api" +) + +var testAnalyzer = &analysis.DefaultAnalyzer{ + Tokenizer: regexpTokenizer.NewRegexpTokenizer(regexp.MustCompile(`\w+`)), +} + +func TestIndexOpenReopen(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // opening the database should have inserted a version + expectedLength := uint64(1) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } + + // now close it + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + idx, err = NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + // now close it + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsert(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // should have 4 rows (1 for version, 1 for schema field, and 1 for single term, and 1 for the term count, and 1 for the back index entry) + expectedLength := uint64(1 + 1 + 1 + 1 + 1) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } +} + +func TestIndexInsertThenDelete(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc2 := document.NewDocument("2") + doc2.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc2) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + err = idx.Delete("1") + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + expectedCount-- + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + err = idx.Delete("2") + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + expectedCount-- + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // should have 2 rows (1 for version, 1 for schema field, 1 for dictionary row garbage) + expectedLength := uint64(1 + 1 + 1) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } +} + +func TestIndexInsertThenUpdate(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + // this update should overwrite one term, and introduce one new one + doc = document.NewDocument("1") + doc.AddField(document.NewTextFieldWithAnalyzer("name", []uint64{}, []byte("test fail"), testAnalyzer)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + + // should have 2 rows (1 for version, 1 for schema field, and 2 for the two term, and 2 for the term counts, and 1 for the back index entry) + expectedLength := uint64(1 + 1 + 2 + 2 + 1) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } + + // now do another update that should remove one of the terms + doc = document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("fail"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + + // should have 2 rows (1 for version, 1 for schema field, and 1 for the remaining term, and 2 for the term diciontary, and 1 for the back index entry) + expectedLength = uint64(1 + 1 + 1 + 2 + 1) + rowCount, err = idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } +} + +func TestIndexInsertMultiple(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + + var expectedCount uint64 + + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + // should have 4 rows (1 for version, 1 for schema field, and 2 for single term, and 1 for the term count, and 2 for the back index entries) + expectedLength := uint64(1 + 1 + 2 + 1 + 2) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } + + // close, reopen and add one more to test that counting works correctly + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + idx, err = NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Fatalf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc = document.NewDocument("3") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsertWithStore(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // should have 6 rows (1 for version, 1 for schema field, and 1 for single term, and 1 for the stored field and 1 for the term count, and 1 for the back index entry) + expectedLength := uint64(1 + 1 + 1 + 1 + 1 + 1) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + storedDocInt, err := indexReader.Document("1") + if err != nil { + t.Error(err) + } + storedDoc := storedDocInt.(*document.Document) + + if len(storedDoc.Fields) != 1 { + t.Errorf("expected 1 stored field, got %d", len(storedDoc.Fields)) + } + textField, ok := storedDoc.Fields[0].(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "test" { + t.Errorf("expected field content 'test', got '%s'", string(textField.Value())) + } +} + +func TestIndexInternalCRUD(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + + // get something that doesn't exist yet + val, err := indexReader.GetInternal([]byte("key")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got %s", val) + } + + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + + // set + err = idx.SetInternal([]byte("key"), []byte("abc")) + if err != nil { + t.Error(err) + } + + indexReader2, err := idx.Reader() + if err != nil { + t.Error(err) + } + + // get + val, err = indexReader2.GetInternal([]byte("key")) + if err != nil { + t.Error(err) + } + if string(val) != "abc" { + t.Errorf("expected %s, got '%s'", "abc", val) + } + + err = indexReader2.Close() + if err != nil { + t.Fatal(err) + } + + // delete + err = idx.DeleteInternal([]byte("key")) + if err != nil { + t.Error(err) + } + + indexReader3, err := idx.Reader() + if err != nil { + t.Error(err) + } + + // get again + val, err = indexReader3.GetInternal([]byte("key")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got %s", val) + } + + err = indexReader3.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexBatch(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + + // first create 2 docs the old fashioned way + doc := document.NewDocument("1") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2"))) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + // now create a batch which does 3 things + // insert new doc + // update existing doc + // delete existing doc + // net document count change 0 + + batch := index.NewBatch() + doc = document.NewDocument("3") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test3"))) + batch.Update(doc) + doc = document.NewDocument("2") + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test2updated"))) + batch.Update(doc) + batch.Delete("1") + + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + docCount, err := indexReader.DocCount() + if err != nil { + t.Fatal(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + + docIDReader, err := indexReader.DocIDReaderAll() + if err != nil { + t.Error(err) + } + var docIds []index.IndexInternalID + docID, err := docIDReader.Next() + for docID != nil && err == nil { + docIds = append(docIds, docID) + docID, err = docIDReader.Next() + } + if err != nil { + t.Error(err) + } + expectedDocIds := []index.IndexInternalID{index.IndexInternalID("2"), index.IndexInternalID("3")} + if !reflect.DeepEqual(docIds, expectedDocIds) { + t.Errorf("expected ids: %v, got ids: %v", expectedDocIds, docIds) + } +} + +func TestIndexInsertUpdateDeleteWithMultipleTypesStored(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var expectedCount uint64 + reader, err := idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err := reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 35.99, index.IndexField|index.StoreField)) + df, err := document.NewDateTimeFieldWithIndexingOptions("unixEpoch", []uint64{}, time.Unix(0, 0), time.RFC3339, index.IndexField|index.StoreField) + if err != nil { + t.Error(err) + } + doc.AddField(df) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + expectedCount++ + + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } + + // should have 72 rows + // 1 for version + // 3 for schema fields + // 1 for text term + // 16 for numeric terms + // 16 for date terms + // 3 for the stored field + // 1 for the text term count + // 16 for numeric term counts + // 16 for date term counts + // 1 for the back index entry + expectedLength := uint64(1 + 3 + 1 + (64 / document.DefaultPrecisionStep) + (64 / document.DefaultPrecisionStep) + 3 + 1 + (64 / document.DefaultPrecisionStep) + (64 / document.DefaultPrecisionStep) + 1) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + + storedDocInt, err := indexReader.Document("1") + if err != nil { + t.Error(err) + } + storedDoc := storedDocInt.(*document.Document) + + err = indexReader.Close() + if err != nil { + t.Error(err) + } + + if len(storedDoc.Fields) != 3 { + t.Errorf("expected 3 stored field, got %d", len(storedDoc.Fields)) + } + textField, ok := storedDoc.Fields[0].(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "test" { + t.Errorf("expected field content 'test', got '%s'", string(textField.Value())) + } + numField, ok := storedDoc.Fields[1].(*document.NumericField) + if !ok { + t.Errorf("expected numeric field") + } + numFieldNumer, err := numField.Number() + if err != nil { + t.Error(err) + } else { + if numFieldNumer != 35.99 { + t.Errorf("expected numeric value 35.99, got %f", numFieldNumer) + } + } + dateField, ok := storedDoc.Fields[2].(*document.DateTimeField) + if !ok { + t.Errorf("expected date field") + } + dateFieldDate, _, err := dateField.DateTime() + if err != nil { + t.Error(err) + } else { + if dateFieldDate != time.Unix(0, 0).UTC() { + t.Errorf("expected date value unix epoch, got %v", dateFieldDate) + } + } + + // now update the document, but omit one of the fields + doc = document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("testup"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 36.99, index.IndexField|index.StoreField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader2, err := idx.Reader() + if err != nil { + t.Error(err) + } + + // expected doc count shouldn't have changed + docCount, err = indexReader2.DocCount() + if err != nil { + t.Fatal(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + + // should only get 2 fields back now though + storedDocInt, err = indexReader2.Document("1") + if err != nil { + t.Error(err) + } + storedDoc = storedDocInt.(*document.Document) + + err = indexReader2.Close() + if err != nil { + t.Error(err) + } + + if len(storedDoc.Fields) != 2 { + t.Errorf("expected 3 stored field, got %d", len(storedDoc.Fields)) + } + textField, ok = storedDoc.Fields[0].(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "testup" { + t.Errorf("expected field content 'testup', got '%s'", string(textField.Value())) + } + numField, ok = storedDoc.Fields[1].(*document.NumericField) + if !ok { + t.Errorf("expected numeric field") + } + numFieldNumer, err = numField.Number() + if err != nil { + t.Error(err) + } else { + if numFieldNumer != 36.99 { + t.Errorf("expected numeric value 36.99, got %f", numFieldNumer) + } + } + + // now delete the document + err = idx.Delete("1") + if err != nil { + t.Errorf("Error deleting entry from index: %v", err) + } + + expectedCount-- + + // expected doc count shouldn't have changed + reader, err = idx.Reader() + if err != nil { + t.Fatal(err) + } + docCount, err = reader.DocCount() + if err != nil { + t.Error(err) + } + if docCount != expectedCount { + t.Errorf("Expected document count to be %d got %d", expectedCount, docCount) + } + err = reader.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexInsertFields(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewNumericFieldWithIndexingOptions("age", []uint64{}, 35.99, index.IndexField|index.StoreField)) + dateField, err := document.NewDateTimeFieldWithIndexingOptions("unixEpoch", []uint64{}, time.Unix(0, 0), time.RFC3339, index.IndexField|index.StoreField) + if err != nil { + t.Error(err) + } + doc.AddField(dateField) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + fields, err := indexReader.Fields() + if err != nil { + t.Error(err) + } else { + expectedFields := []string{"name", "age", "unixEpoch"} + if !reflect.DeepEqual(fields, expectedFields) { + t.Errorf("expected fields: %v, got %v", expectedFields, fields) + } + } +} + +func TestIndexUpdateComposites(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField)) + doc.AddField(document.NewCompositeFieldWithIndexingOptions("_all", true, nil, nil, index.IndexField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + // should have 72 rows + // 1 for version + // 3 for schema fields + // 4 for text term + // 2 for the stored field + // 4 for the text term count + // 1 for the back index entry + expectedLength := uint64(1 + 3 + 4 + 2 + 4 + 1) + rowCount, err := idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } + + // now lets update it + doc = document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("testupdated"), index.IndexField|index.StoreField)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("misterupdated"), index.IndexField|index.StoreField)) + doc.AddField(document.NewCompositeFieldWithIndexingOptions("_all", true, nil, nil, index.IndexField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + // make sure new values are in index + storedDocInt, err := indexReader.Document("1") + if err != nil { + t.Error(err) + } + storedDoc := storedDocInt.(*document.Document) + if len(storedDoc.Fields) != 2 { + t.Errorf("expected 2 stored field, got %d", len(storedDoc.Fields)) + } + textField, ok := storedDoc.Fields[0].(*document.TextField) + if !ok { + t.Errorf("expected text field") + } + if string(textField.Value()) != "testupdated" { + t.Errorf("expected field content 'test', got '%s'", string(textField.Value())) + } + + // should have the same row count as before, plus 4 term dictionary garbage rows + expectedLength += 4 + rowCount, err = idx.(*UpsideDownCouch).rowCount() + if err != nil { + t.Error(err) + } + if rowCount != expectedLength { + t.Errorf("expected %d rows, got: %d", expectedLength, rowCount) + } +} + +func TestIndexFieldsMisc(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + fieldName1 := idx.(*UpsideDownCouch).fieldCache.FieldIndexed(0) + if fieldName1 != "name" { + t.Errorf("expected field named 'name', got '%s'", fieldName1) + } + fieldName2 := idx.(*UpsideDownCouch).fieldCache.FieldIndexed(1) + if fieldName2 != "title" { + t.Errorf("expected field named 'title', got '%s'", fieldName2) + } + fieldName3 := idx.(*UpsideDownCouch).fieldCache.FieldIndexed(2) + if fieldName3 != "" { + t.Errorf("expected field named '', got '%s'", fieldName3) + } +} + +func TestIndexTermReaderCompositeFields(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc.AddField(document.NewCompositeFieldWithIndexingOptions("_all", true, nil, nil, index.IndexField|index.IncludeTermVectors)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + termFieldReader, err := indexReader.TermFieldReader(context.TODO(), []byte("mister"), "_all", true, true, true) + if err != nil { + t.Error(err) + } + + tfd, err := termFieldReader.Next(nil) + for tfd != nil && err == nil { + if !tfd.ID.Equals(index.IndexInternalID("1")) { + t.Errorf("expected to find document id 1") + } + tfd, err = termFieldReader.Next(nil) + } + if err != nil { + t.Error(err) + } +} + +func TestIndexDocValueReader(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions("name", []uint64{}, []byte("test"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + doc.AddField(document.NewTextFieldWithIndexingOptions("title", []uint64{}, []byte("mister"), index.IndexField|index.StoreField|index.IncludeTermVectors)) + err = idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + + indexReader, err := idx.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + actualFieldTerms := make(fieldTerms) + + dvr, err := indexReader.DocValueReader([]string{"name", "title"}) + if err != nil { + t.Error(err) + } + + err = dvr.VisitDocValues(index.IndexInternalID("1"), func(field string, term []byte) { + actualFieldTerms[field] = append(actualFieldTerms[field], string(term)) + }) + if err != nil { + t.Error(err) + } + expectedFieldTerms := fieldTerms{ + "name": []string{"test"}, + "title": []string{"mister"}, + } + if !reflect.DeepEqual(actualFieldTerms, expectedFieldTerms) { + t.Errorf("expected field terms: %#v, got: %#v", expectedFieldTerms, actualFieldTerms) + } +} + +func BenchmarkBatch(b *testing.B) { + cache := registry.NewCache() + analyzer, err := cache.AnalyzerNamed(standard.Name) + if err != nil { + b.Fatal(err) + } + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(null.Name, nil, analysisQueue) + if err != nil { + b.Fatal(err) + } + err = idx.Open() + if err != nil { + b.Fatal(err) + } + + batch := index.NewBatch() + for i := 0; i < 100; i++ { + d := document.NewDocument(strconv.Itoa(i)) + f := document.NewTextFieldWithAnalyzer("desc", nil, bleveWikiArticle1K, analyzer) + d.AddField(f) + batch.Update(d) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + err = idx.Batch(batch) + if err != nil { + b.Fatal(err) + } + } +} + +func TestConcurrentUpdate(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // do some concurrent updates + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(i int) { + doc := document.NewDocument("1") + doc.AddField(document.NewTextFieldWithIndexingOptions(strconv.Itoa(i), []uint64{}, []byte(strconv.Itoa(i)), index.StoreField)) + err := idx.Update(doc) + if err != nil { + t.Errorf("Error updating index: %v", err) + } + wg.Done() + }(i) + } + wg.Wait() + + // now load the name field and see what we get + r, err := idx.Reader() + if err != nil { + log.Fatal(err) + } + + docInt, err := r.Document("1") + if err != nil { + log.Fatal(err) + } + doc := docInt.(*document.Document) + + if len(doc.Fields) > 1 { + t.Errorf("expected single field, found %d", len(doc.Fields)) + } + + err = r.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestLargeField(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var largeFieldValue []byte + for len(largeFieldValue) < RowBufferSize { + largeFieldValue = append(largeFieldValue, bleveWikiArticle1K...) + } + t.Logf("large field size: %d", len(largeFieldValue)) + + d := document.NewDocument("large") + f := document.NewTextFieldWithIndexingOptions("desc", nil, largeFieldValue, index.IndexField|index.StoreField) + d.AddField(f) + + err = idx.Update(d) + if err != nil { + t.Fatal(err) + } +} + +func TestIndexBatchPersistedCallbackWithErrorUpsideDown(t *testing.T) { + defer func() { + err := DestroyTest() + if err != nil { + t.Fatal(err) + } + }() + + analysisQueue := index.NewAnalysisQueue(1) + idx, err := NewUpsideDownCouch(boltdb.Name, boltTestConfig, analysisQueue) + if err != nil { + t.Fatal(err) + } + err = idx.Open() + if err != nil { + t.Errorf("error opening index: %v", err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + var callbackExecuted bool + batch := index.NewBatch() + batch.SetPersistedCallback(func(e error) { + callbackExecuted = true + }) + // By using a really large ID, we ensure that the batch will fail, + // because the key generated by upside down will be too large for BoltDB + reallyBigId := strings.Repeat("x", 32768+1) + doc := document.NewDocument(reallyBigId) + doc.AddField(document.NewTextField("name", []uint64{}, []byte("test3"))) + batch.Update(doc) + + _ = idx.Batch(batch) + // don't fail on this error, that isn't what we're testing + + if !callbackExecuted { + t.Fatal("expected callback to fire, it did not") + } +} + +// fieldTerms contains the terms used by a document, keyed by field +type fieldTerms map[string][]string + +// FieldsNotYetCached returns a list of fields not yet cached out of a larger list of fields +func (f fieldTerms) FieldsNotYetCached(fields []string) []string { + rv := make([]string, 0, len(fields)) + for _, field := range fields { + if _, ok := f[field]; !ok { + rv = append(rv, field) + } + } + return rv +} + +// Merge will combine two fieldTerms +// it assumes that the terms lists are complete (thus do not need to be merged) +// field terms from the other list always replace the ones in the receiver +func (f fieldTerms) Merge(other fieldTerms) { + for field, terms := range other { + f[field] = terms + } +} diff --git a/index_alias.go b/index_alias.go new file mode 100644 index 0000000..7a85d72 --- /dev/null +++ b/index_alias.go @@ -0,0 +1,37 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +// An IndexAlias is a wrapper around one or more +// Index objects. It has two distinct modes of +// operation. +// 1. When it points to a single index, ALL index +// operations are valid and will be passed through +// to the underlying index. +// 2. When it points to more than one index, the only +// valid operation is Search. In this case the +// search will be performed across all the +// underlying indexes and the results merged. +// Calls to Add/Remove/Swap the underlying indexes +// are atomic, so you can safely change the +// underlying Index objects while other components +// are performing operations. +type IndexAlias interface { + Index + + Add(i ...Index) + Remove(i ...Index) + Swap(in, out []Index) +} diff --git a/index_alias_impl.go b/index_alias_impl.go new file mode 100644 index 0000000..7b49b8a --- /dev/null +++ b/index_alias_impl.go @@ -0,0 +1,1067 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/collector" + "github.com/blevesearch/bleve/v2/search/query" + index "github.com/blevesearch/bleve_index_api" +) + +type indexAliasImpl struct { + name string + indexes []Index + mutex sync.RWMutex + open bool + // if all the indexes in tha alias have the same mapping + // then the user can set the mapping here to avoid + // checking the mapping of each index in the alias + mapping mapping.IndexMapping +} + +// NewIndexAlias creates a new IndexAlias over the provided +// Index objects. +func NewIndexAlias(indexes ...Index) *indexAliasImpl { + return &indexAliasImpl{ + name: "alias", + indexes: indexes, + open: true, + } +} + +// VisitIndexes invokes the visit callback on every +// indexes included in the index alias. +func (i *indexAliasImpl) VisitIndexes(visit func(Index)) { + i.mutex.RLock() + for _, idx := range i.indexes { + visit(idx) + } + i.mutex.RUnlock() +} + +func (i *indexAliasImpl) isAliasToSingleIndex() error { + if len(i.indexes) < 1 { + return ErrorAliasEmpty + } else if len(i.indexes) > 1 { + return ErrorAliasMulti + } + return nil +} + +func (i *indexAliasImpl) Index(id string, data interface{}) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return err + } + + return i.indexes[0].Index(id, data) +} + +func (i *indexAliasImpl) IndexSynonym(id string, collection string, definition *SynonymDefinition) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return err + } + + if si, ok := i.indexes[0].(SynonymIndex); ok { + return si.IndexSynonym(id, collection, definition) + } + return ErrorSynonymSearchNotSupported +} + +func (i *indexAliasImpl) Delete(id string) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return err + } + + return i.indexes[0].Delete(id) +} + +func (i *indexAliasImpl) Batch(b *Batch) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return err + } + + return i.indexes[0].Batch(b) +} + +func (i *indexAliasImpl) Document(id string) (index.Document, error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil, err + } + + return i.indexes[0].Document(id) +} + +func (i *indexAliasImpl) DocCount() (uint64, error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + rv := uint64(0) + + if !i.open { + return 0, ErrorIndexClosed + } + + for _, index := range i.indexes { + otherCount, err := index.DocCount() + if err == nil { + rv += otherCount + } + // tolerate errors to produce partial counts + } + + return rv, nil +} + +func (i *indexAliasImpl) Search(req *SearchRequest) (*SearchResult, error) { + return i.SearchInContext(context.Background(), req) +} + +func (i *indexAliasImpl) SearchInContext(ctx context.Context, req *SearchRequest) (*SearchResult, error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + + if len(i.indexes) < 1 { + return nil, ErrorAliasEmpty + } + if _, ok := ctx.Value(search.PreSearchKey).(bool); ok { + // since preSearchKey is set, it means that the request + // is being executed as part of a preSearch, which + // indicates that this index alias is set as an Index + // in another alias, so we need to do a preSearch search + // and NOT a real search + bm25PreSearch := isBM25Enabled(i.mapping) + flags := &preSearchFlags{ + knn: requestHasKNN(req), + synonyms: !isMatchNoneQuery(req.Query), + bm25: bm25PreSearch, + } + return preSearchDataSearch(ctx, req, flags, i.indexes...) + } + + // at this point we know we are doing a real search + // either after a preSearch is done, or directly + // on the alias + + // check if request has preSearchData which would indicate that the + // request has already been preSearched and we can skip the + // preSearch step now, we call an optional function to + // redistribute the preSearchData to the individual indexes + // if necessary + var preSearchData map[string]map[string]interface{} + if req.PreSearchData != nil { + var err error + preSearchData, err = redistributePreSearchData(req, i.indexes) + if err != nil { + return nil, err + } + } + + // short circuit the simple case + if len(i.indexes) == 1 { + if preSearchData != nil { + req.PreSearchData = preSearchData[i.indexes[0].Name()] + } + return i.indexes[0].SearchInContext(ctx, req) + } + + // at this stage we know we have multiple indexes + // check if preSearchData needs to be gathered from all indexes + // before executing the query + var err error + // only perform preSearch if + // - the request does not already have preSearchData + // - the request requires preSearch + var preSearchDuration time.Duration + var sr *SearchResult + flags, err := preSearchRequired(ctx, req, i.mapping) + if err != nil { + return nil, err + } + if req.PreSearchData == nil && flags != nil { + searchStart := time.Now() + preSearchResult, err := preSearch(ctx, req, flags, i.indexes...) + if err != nil { + return nil, err + } + + // check if the preSearch result has any errors and if so + // return the search result as is without executing the query + // so that the errors are not lost + if preSearchResult.Status.Failed > 0 || len(preSearchResult.Status.Errors) > 0 { + return preSearchResult, nil + } + // finalize the preSearch result now + finalizePreSearchResult(req, flags, preSearchResult) + + // if there are no errors, then merge the data in the preSearch result + // and construct the preSearchData to be used in the actual search + // if the request is satisfied by the preSearch result, then we can + // directly return the preSearch result as the final result + if requestSatisfiedByPreSearch(req, flags) { + sr = finalizeSearchResult(req, preSearchResult) + // no need to run the 2nd phase MultiSearch(..) + } else { + preSearchData, err = constructPreSearchData(req, flags, preSearchResult, i.indexes) + if err != nil { + return nil, err + } + } + preSearchDuration = time.Since(searchStart) + } + + // check if search result was generated as part of preSearch itself + if sr == nil { + sr, err = MultiSearch(ctx, req, preSearchData, i.indexes...) + if err != nil { + return nil, err + } + } + sr.Took += preSearchDuration + return sr, nil +} + +func (i *indexAliasImpl) Fields() ([]string, error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil, err + } + + return i.indexes[0].Fields() +} + +func (i *indexAliasImpl) FieldDict(field string) (index.FieldDict, error) { + i.mutex.RLock() + + if !i.open { + i.mutex.RUnlock() + return nil, ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + fieldDict, err := i.indexes[0].FieldDict(field) + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + return &indexAliasImplFieldDict{ + index: i, + fieldDict: fieldDict, + }, nil +} + +func (i *indexAliasImpl) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) { + i.mutex.RLock() + + if !i.open { + i.mutex.RUnlock() + return nil, ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + fieldDict, err := i.indexes[0].FieldDictRange(field, startTerm, endTerm) + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + return &indexAliasImplFieldDict{ + index: i, + fieldDict: fieldDict, + }, nil +} + +func (i *indexAliasImpl) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) { + i.mutex.RLock() + + if !i.open { + i.mutex.RUnlock() + return nil, ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + fieldDict, err := i.indexes[0].FieldDictPrefix(field, termPrefix) + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + return &indexAliasImplFieldDict{ + index: i, + fieldDict: fieldDict, + }, nil +} + +func (i *indexAliasImpl) Close() error { + i.mutex.Lock() + defer i.mutex.Unlock() + + i.open = false + return nil +} + +// SetIndexMapping sets the mapping for the alias and must be used +// ONLY when all the indexes in the alias have the same mapping. +// This is to avoid checking the mapping of each index in the alias +// when executing a search request. +func (i *indexAliasImpl) SetIndexMapping(m mapping.IndexMapping) error { + i.mutex.Lock() + defer i.mutex.Unlock() + if !i.open { + return ErrorIndexClosed + } + i.mapping = m + return nil +} + +func (i *indexAliasImpl) Mapping() mapping.IndexMapping { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil + } + + // if the mapping is already set, return it + if i.mapping != nil { + return i.mapping + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil + } + + return i.indexes[0].Mapping() +} + +func (i *indexAliasImpl) Stats() *IndexStat { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil + } + + return i.indexes[0].Stats() +} + +func (i *indexAliasImpl) StatsMap() map[string]interface{} { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil + } + + return i.indexes[0].StatsMap() +} + +func (i *indexAliasImpl) GetInternal(key []byte) ([]byte, error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil, err + } + + return i.indexes[0].GetInternal(key) +} + +func (i *indexAliasImpl) SetInternal(key, val []byte) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return err + } + + return i.indexes[0].SetInternal(key, val) +} + +func (i *indexAliasImpl) DeleteInternal(key []byte) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return err + } + + return i.indexes[0].DeleteInternal(key) +} + +func (i *indexAliasImpl) Advanced() (index.Index, error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil, err + } + + return i.indexes[0].Advanced() +} + +func (i *indexAliasImpl) Add(indexes ...Index) { + i.mutex.Lock() + defer i.mutex.Unlock() + + i.indexes = append(i.indexes, indexes...) +} + +func (i *indexAliasImpl) removeSingle(index Index) { + for pos, in := range i.indexes { + if in == index { + i.indexes = append(i.indexes[:pos], i.indexes[pos+1:]...) + break + } + } +} + +func (i *indexAliasImpl) Remove(indexes ...Index) { + i.mutex.Lock() + defer i.mutex.Unlock() + + for _, in := range indexes { + i.removeSingle(in) + } +} + +func (i *indexAliasImpl) Swap(in, out []Index) { + i.mutex.Lock() + defer i.mutex.Unlock() + + // add + i.indexes = append(i.indexes, in...) + + // delete + for _, ind := range out { + i.removeSingle(ind) + } +} + +// createChildSearchRequest creates a separate +// request from the original +// For now, avoid data race on req structure. +// TODO disable highlight/field load on child +// requests, and add code to do this only on +// the actual final results. +// Perhaps that part needs to be optional, +// could be slower in remote usages. +func createChildSearchRequest(req *SearchRequest, preSearchData map[string]interface{}) *SearchRequest { + return copySearchRequest(req, preSearchData) +} + +type asyncSearchResult struct { + Name string + Result *SearchResult + Err error +} + +// preSearchFlags is a struct to hold flags indicating why preSearch is required +type preSearchFlags struct { + knn bool + synonyms bool + bm25 bool // needs presearch for this too +} + +func isBM25Enabled(m mapping.IndexMapping) bool { + var rv bool + if m, ok := m.(*mapping.IndexMappingImpl); ok { + rv = m.ScoringModel == index.BM25Scoring + } + return rv +} + +// preSearchRequired checks if preSearch is required and returns the presearch flags struct +// indicating which preSearch is required +func preSearchRequired(ctx context.Context, req *SearchRequest, m mapping.IndexMapping) (*preSearchFlags, error) { + // Check for KNN query + knn := requestHasKNN(req) + var synonyms bool + if !isMatchNoneQuery(req.Query) { + // Check if synonyms are defined in the mapping + if sm, ok := m.(mapping.SynonymMapping); ok && sm.SynonymCount() > 0 { + // check if any of the fields queried have a synonym source + // in the index mapping, to prevent unnecessary preSearch + fs, err := query.ExtractFields(req.Query, m, nil) + if err != nil { + return nil, err + } + for field := range fs { + if sm.SynonymSourceForPath(field) != "" { + synonyms = true + break + } + } + } + } + var bm25 bool + if !isMatchNoneQuery(req.Query) { + if ctx != nil { + if searchType := ctx.Value(search.SearchTypeKey); searchType != nil { + if searchType.(string) == search.GlobalScoring { + bm25 = isBM25Enabled(m) + } + } + } + } + + if knn || synonyms || bm25 { + return &preSearchFlags{ + knn: knn, + synonyms: synonyms, + bm25: bm25, + }, nil + } + return nil, nil +} + +func preSearch(ctx context.Context, req *SearchRequest, flags *preSearchFlags, indexes ...Index) (*SearchResult, error) { + // create a dummy request with a match none query + // since we only care about the preSearchData in PreSearch + dummyQuery := req.Query + if !flags.bm25 && !flags.synonyms { + // create a dummy request with a match none query + // since we only care about the preSearchData in PreSearch + dummyQuery = query.NewMatchNoneQuery() + } + dummyRequest := &SearchRequest{ + Query: dummyQuery, + } + newCtx := context.WithValue(ctx, search.PreSearchKey, true) + if flags.knn { + addKnnToDummyRequest(dummyRequest, req) + } + return preSearchDataSearch(newCtx, dummyRequest, flags, indexes...) +} + +// if the request is satisfied by just the preSearch result, +// finalize the result and return it directly without +// performing multi search +func finalizeSearchResult(req *SearchRequest, preSearchResult *SearchResult) *SearchResult { + if preSearchResult == nil { + return nil + } + + // global values across all hits irrespective of pagination settings + preSearchResult.Total = uint64(preSearchResult.Hits.Len()) + maxScore := float64(0) + for i, hit := range preSearchResult.Hits { + // since we are now using the preSearch result as the final result + // we can discard the indexNames from the hits as they are no longer + // relevant. + hit.IndexNames = nil + if hit.Score > maxScore { + maxScore = hit.Score + } + hit.HitNumber = uint64(i) + } + preSearchResult.MaxScore = maxScore + // now apply pagination settings + var reverseQueryExecution bool + if req.SearchBefore != nil { + reverseQueryExecution = true + req.Sort.Reverse() + req.SearchAfter = req.SearchBefore + } + if req.SearchAfter != nil { + preSearchResult.Hits = collector.FilterHitsBySearchAfter(preSearchResult.Hits, req.Sort, req.SearchAfter) + } + preSearchResult.Hits = hitsInCurrentPage(req, preSearchResult.Hits) + if reverseQueryExecution { + // reverse the sort back to the original + req.Sort.Reverse() + // resort using the original order + mhs := newSearchHitSorter(req.Sort, preSearchResult.Hits) + req.SortFunc()(mhs) + req.SearchAfter = nil + } + + if req.Explain { + preSearchResult.Request = req + } + return preSearchResult +} + +func requestSatisfiedByPreSearch(req *SearchRequest, flags *preSearchFlags) bool { + if flags == nil { + return false + } + // if the synonyms presearch flag is set the request can never be satisfied by + // the preSearch result as synonyms are not part of the preSearch result + if flags.synonyms { + return false + } + if flags.knn && isKNNrequestSatisfiedByPreSearch(req) { + return true + } + return false +} + +func constructSynonymPreSearchData(rv map[string]map[string]interface{}, sr *SearchResult, indexes []Index) map[string]map[string]interface{} { + for _, index := range indexes { + rv[index.Name()][search.SynonymPreSearchDataKey] = sr.SynonymResult + } + return rv +} + +func constructBM25PreSearchData(rv map[string]map[string]interface{}, sr *SearchResult, indexes []Index) map[string]map[string]interface{} { + bmStats := sr.BM25Stats + if bmStats != nil { + for _, index := range indexes { + rv[index.Name()][search.BM25PreSearchDataKey] = &search.BM25Stats{ + DocCount: bmStats.DocCount, + FieldCardinality: bmStats.FieldCardinality, + } + } + } + return rv +} + +func constructPreSearchData(req *SearchRequest, flags *preSearchFlags, + preSearchResult *SearchResult, indexes []Index, +) (map[string]map[string]interface{}, error) { + if flags == nil || preSearchResult == nil { + return nil, fmt.Errorf("invalid input, flags: %v, preSearchResult: %v", flags, preSearchResult) + } + mergedOut := make(map[string]map[string]interface{}, len(indexes)) + for _, index := range indexes { + mergedOut[index.Name()] = make(map[string]interface{}) + } + var err error + if flags.knn { + mergedOut, err = constructKnnPreSearchData(mergedOut, preSearchResult, indexes) + if err != nil { + return nil, err + } + } + if flags.synonyms { + mergedOut = constructSynonymPreSearchData(mergedOut, preSearchResult, indexes) + } + if flags.bm25 { + mergedOut = constructBM25PreSearchData(mergedOut, preSearchResult, indexes) + } + return mergedOut, nil +} + +func preSearchDataSearch(ctx context.Context, req *SearchRequest, flags *preSearchFlags, indexes ...Index) (*SearchResult, error) { + asyncResults := make(chan *asyncSearchResult, len(indexes)) + // run search on each index in separate go routine + var waitGroup sync.WaitGroup + searchChildIndex := func(in Index, childReq *SearchRequest) { + rv := asyncSearchResult{Name: in.Name()} + rv.Result, rv.Err = in.SearchInContext(ctx, childReq) + asyncResults <- &rv + waitGroup.Done() + } + waitGroup.Add(len(indexes)) + for _, in := range indexes { + go searchChildIndex(in, createChildSearchRequest(req, nil)) + } + // on another go routine, close after finished + go func() { + waitGroup.Wait() + close(asyncResults) + }() + // the final search result to be returned after combining the preSearch results + var sr *SearchResult + // the preSearch result processor + var prp preSearchResultProcessor + // error map + indexErrors := make(map[string]error) + for asr := range asyncResults { + if asr.Err == nil { + // a valid preSearch result + if prp == nil { + // first valid preSearch result + // create a new preSearch result processor + prp = createPreSearchResultProcessor(req, flags) + } + prp.add(asr.Result, asr.Name) + if sr == nil { + // first result + sr = &SearchResult{ + Status: asr.Result.Status, + Cost: asr.Result.Cost, + } + } else { + // merge with previous + sr.Status.Merge(asr.Result.Status) + sr.Cost += asr.Result.Cost + } + } else { + indexErrors[asr.Name] = asr.Err + } + } + // handle case where no results were successful + if sr == nil { + sr = &SearchResult{ + Status: &SearchStatus{ + Errors: make(map[string]error), + }, + } + } + // in preSearch, partial results are not allowed as it can lead to + // the real search giving incorrect results, and hence the search + // result is not populated with any of the processed data from + // the preSearch result processor if there are any errors + // or the preSearch result status has any failures + if len(indexErrors) > 0 || sr.Status.Failed > 0 { + if sr.Status.Errors == nil { + sr.Status.Errors = make(map[string]error) + } + for indexName, indexErr := range indexErrors { + sr.Status.Errors[indexName] = indexErr + sr.Status.Total++ + } + // At this point, all errors have been recorded—either from the preSearch phase + // (via status.Merge) or from individual index search failures (indexErrors). + // Since partial results are not allowed, mark the entire request as failed. + sr.Status.Successful = 0 + sr.Status.Failed = sr.Status.Total + } else { + prp.finalize(sr) + } + return sr, nil +} + +// redistributePreSearchData redistributes the preSearchData sent in the search request to an index alias +// which would happen in the case of an alias tree and depending on the level of the tree, the preSearchData +// needs to be redistributed to the indexes at that level +func redistributePreSearchData(req *SearchRequest, indexes []Index) (map[string]map[string]interface{}, error) { + rv := make(map[string]map[string]interface{}) + for _, index := range indexes { + rv[index.Name()] = make(map[string]interface{}) + } + if knnHits, ok := req.PreSearchData[search.KnnPreSearchDataKey].([]*search.DocumentMatch); ok { + // the preSearchData for KNN is a list of DocumentMatch objects + // that need to be redistributed to the right index. + // This is used only in the case of an alias tree, where the indexes + // are at the leaves of the tree, and the master alias is at the root. + // At each level of the tree, the preSearchData needs to be redistributed + // to the indexes/aliases at that level. Because the preSearchData is + // specific to each final index at the leaf. + segregatedKnnHits, err := validateAndDistributeKNNHits(knnHits, indexes) + if err != nil { + return nil, err + } + for _, index := range indexes { + rv[index.Name()][search.KnnPreSearchDataKey] = segregatedKnnHits[index.Name()] + } + } + if fts, ok := req.PreSearchData[search.SynonymPreSearchDataKey].(search.FieldTermSynonymMap); ok { + for _, index := range indexes { + rv[index.Name()][search.SynonymPreSearchDataKey] = fts + } + } + + if bm25Data, ok := req.PreSearchData[search.BM25PreSearchDataKey].(*search.BM25Stats); ok { + for _, index := range indexes { + rv[index.Name()][search.BM25PreSearchDataKey] = bm25Data + } + } + return rv, nil +} + +// finalizePreSearchResult finalizes the preSearch result by applying the finalization steps +// specific to the preSearch flags +func finalizePreSearchResult(req *SearchRequest, flags *preSearchFlags, preSearchResult *SearchResult) { + // if flags is nil then return + if flags == nil { + return + } + if flags.knn { + preSearchResult.Hits = finalizeKNNResults(req, preSearchResult.Hits) + } +} + +// hitsInCurrentPage returns the hits in the current page +// using the From and Size parameters in the request +func hitsInCurrentPage(req *SearchRequest, hits []*search.DocumentMatch) []*search.DocumentMatch { + sortFunc := req.SortFunc() + // sort all hits with the requested order + if len(req.Sort) > 0 { + sorter := newSearchHitSorter(req.Sort, hits) + sortFunc(sorter) + } + // now skip over the correct From + if req.From > 0 && len(hits) > req.From { + hits = hits[req.From:] + } else if req.From > 0 { + hits = search.DocumentMatchCollection{} + } + // now trim to the correct size + if req.Size > 0 && len(hits) > req.Size { + hits = hits[0:req.Size] + } + return hits +} + +// MultiSearch executes a SearchRequest across multiple Index objects, +// then merges the results. The indexes must honor any ctx deadline. +func MultiSearch(ctx context.Context, req *SearchRequest, preSearchData map[string]map[string]interface{}, indexes ...Index) (*SearchResult, error) { + searchStart := time.Now() + asyncResults := make(chan *asyncSearchResult, len(indexes)) + + var reverseQueryExecution bool + if req.SearchBefore != nil { + reverseQueryExecution = true + req.Sort.Reverse() + req.SearchAfter = req.SearchBefore + req.SearchBefore = nil + } + + // run search on each index in separate go routine + var waitGroup sync.WaitGroup + + searchChildIndex := func(in Index, childReq *SearchRequest) { + rv := asyncSearchResult{Name: in.Name()} + rv.Result, rv.Err = in.SearchInContext(ctx, childReq) + asyncResults <- &rv + waitGroup.Done() + } + + waitGroup.Add(len(indexes)) + for _, in := range indexes { + var payload map[string]interface{} + if preSearchData != nil { + payload = preSearchData[in.Name()] + } + go searchChildIndex(in, createChildSearchRequest(req, payload)) + } + + // on another go routine, close after finished + go func() { + waitGroup.Wait() + close(asyncResults) + }() + + var sr *SearchResult + indexErrors := make(map[string]error) + + for asr := range asyncResults { + if asr.Err == nil { + if sr == nil { + // first result + sr = asr.Result + } else { + // merge with previous + sr.Merge(asr.Result) + } + } else { + indexErrors[asr.Name] = asr.Err + } + } + + // merge just concatenated all the hits + // now lets clean it up + + // handle case where no results were successful + if sr == nil { + sr = &SearchResult{ + Status: &SearchStatus{ + Errors: make(map[string]error), + }, + } + } + + sr.Hits = hitsInCurrentPage(req, sr.Hits) + + // fix up facets + for name, fr := range req.Facets { + sr.Facets.Fixup(name, fr.Size) + } + + if reverseQueryExecution { + // reverse the sort back to the original + req.Sort.Reverse() + // resort using the original order + mhs := newSearchHitSorter(req.Sort, sr.Hits) + req.SortFunc()(mhs) + // reset request + req.SearchBefore = req.SearchAfter + req.SearchAfter = nil + } + + // fix up original request + if req.Explain { + sr.Request = req + } + searchDuration := time.Since(searchStart) + sr.Took = searchDuration + + // fix up errors + if len(indexErrors) > 0 { + if sr.Status.Errors == nil { + sr.Status.Errors = make(map[string]error) + } + for indexName, indexErr := range indexErrors { + sr.Status.Errors[indexName] = indexErr + sr.Status.Total++ + sr.Status.Failed++ + } + } + + return sr, nil +} + +func (i *indexAliasImpl) NewBatch() *Batch { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil + } + + err := i.isAliasToSingleIndex() + if err != nil { + return nil + } + + return i.indexes[0].NewBatch() +} + +func (i *indexAliasImpl) Name() string { + return i.name +} + +func (i *indexAliasImpl) SetName(name string) { + i.name = name +} + +type indexAliasImplFieldDict struct { + index *indexAliasImpl + fieldDict index.FieldDict +} + +func (f *indexAliasImplFieldDict) BytesRead() uint64 { + return f.fieldDict.BytesRead() +} + +func (f *indexAliasImplFieldDict) Next() (*index.DictEntry, error) { + return f.fieldDict.Next() +} + +func (f *indexAliasImplFieldDict) Close() error { + defer f.index.mutex.RUnlock() + return f.fieldDict.Close() +} + +func (f *indexAliasImplFieldDict) Cardinality() int { + return f.fieldDict.Cardinality() +} diff --git a/index_alias_impl_test.go b/index_alias_impl_test.go new file mode 100644 index 0000000..0480139 --- /dev/null +++ b/index_alias_impl_test.go @@ -0,0 +1,1355 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestIndexAliasSingle(t *testing.T) { + expectedError := fmt.Errorf("expected") + ei1 := &stubIndex{ + err: expectedError, + } + + alias := NewIndexAlias(ei1) + + err := alias.Index("a", "a") + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + err = alias.Delete("a") + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + batch := alias.NewBatch() + err = alias.Batch(batch) + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + _, err = alias.Document("a") + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + _, err = alias.Fields() + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + _, err = alias.GetInternal([]byte("a")) + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + err = alias.SetInternal([]byte("a"), []byte("a")) + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + err = alias.DeleteInternal([]byte("a")) + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + mapping := alias.Mapping() + if mapping != nil { + t.Errorf("expected nil, got %v", mapping) + } + + indexStat := alias.Stats() + if indexStat != nil { + t.Errorf("expected nil, got %v", indexStat) + } + + // now a few things that should work + sr := NewSearchRequest(NewTermQuery("test")) + _, err = alias.Search(sr) + if err != expectedError { + t.Errorf("expected %v, got %v", expectedError, err) + } + + count, err := alias.DocCount() + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if count != 0 { + t.Errorf("expected count 0, got %d", count) + } + + // now change the def using add/remove + expectedError2 := fmt.Errorf("expected2") + ei2 := &stubIndex{ + err: expectedError2, + } + + alias.Add(ei2) + alias.Remove(ei1) + + err = alias.Index("a", "a") + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + err = alias.Delete("a") + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + err = alias.Batch(batch) + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + _, err = alias.Document("a") + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + _, err = alias.Fields() + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + _, err = alias.GetInternal([]byte("a")) + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + err = alias.SetInternal([]byte("a"), []byte("a")) + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + err = alias.DeleteInternal([]byte("a")) + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + mapping = alias.Mapping() + if mapping != nil { + t.Errorf("expected nil, got %v", mapping) + } + + indexStat = alias.Stats() + if indexStat != nil { + t.Errorf("expected nil, got %v", indexStat) + } + + // now a few things that should work + _, err = alias.Search(sr) + if err != expectedError2 { + t.Errorf("expected %v, got %v", expectedError2, err) + } + + count, err = alias.DocCount() + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if count != 0 { + t.Errorf("expected count 0, got %d", count) + } + + // now change the def using swap + expectedError3 := fmt.Errorf("expected3") + ei3 := &stubIndex{ + err: expectedError3, + } + + alias.Swap([]Index{ei3}, []Index{ei2}) + + err = alias.Index("a", "a") + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + err = alias.Delete("a") + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + err = alias.Batch(batch) + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + _, err = alias.Document("a") + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + _, err = alias.Fields() + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + _, err = alias.GetInternal([]byte("a")) + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + err = alias.SetInternal([]byte("a"), []byte("a")) + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + err = alias.DeleteInternal([]byte("a")) + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + mapping = alias.Mapping() + if mapping != nil { + t.Errorf("expected nil, got %v", mapping) + } + + indexStat = alias.Stats() + if indexStat != nil { + t.Errorf("expected nil, got %v", indexStat) + } + + // now a few things that should work + _, err = alias.Search(sr) + if err != expectedError3 { + t.Errorf("expected %v, got %v", expectedError3, err) + } + + count, err = alias.DocCount() + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if count != 0 { + t.Errorf("expected count 0, got %d", count) + } +} + +func TestIndexAliasClosed(t *testing.T) { + alias := NewIndexAlias() + err := alias.Close() + if err != nil { + t.Fatal(err) + } + + err = alias.Index("a", "a") + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + err = alias.Delete("a") + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + batch := alias.NewBatch() + err = alias.Batch(batch) + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + _, err = alias.Document("a") + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + _, err = alias.Fields() + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + _, err = alias.GetInternal([]byte("a")) + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + err = alias.SetInternal([]byte("a"), []byte("a")) + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + err = alias.DeleteInternal([]byte("a")) + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + mapping := alias.Mapping() + if mapping != nil { + t.Errorf("expected nil, got %v", mapping) + } + + indexStat := alias.Stats() + if indexStat != nil { + t.Errorf("expected nil, got %v", indexStat) + } + + // now a few things that should work + sr := NewSearchRequest(NewTermQuery("test")) + _, err = alias.Search(sr) + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } + + _, err = alias.DocCount() + if err != ErrorIndexClosed { + t.Errorf("expected %v, got %v", ErrorIndexClosed, err) + } +} + +func TestIndexAliasEmpty(t *testing.T) { + alias := NewIndexAlias() + + err := alias.Index("a", "a") + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + err = alias.Delete("a") + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + batch := alias.NewBatch() + err = alias.Batch(batch) + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + _, err = alias.Document("a") + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + _, err = alias.Fields() + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + _, err = alias.GetInternal([]byte("a")) + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + err = alias.SetInternal([]byte("a"), []byte("a")) + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + err = alias.DeleteInternal([]byte("a")) + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + mapping := alias.Mapping() + if mapping != nil { + t.Errorf("expected nil, got %v", mapping) + } + + indexStat := alias.Stats() + if indexStat != nil { + t.Errorf("expected nil, got %v", indexStat) + } + + // now a few things that should work + sr := NewSearchRequest(NewTermQuery("test")) + _, err = alias.Search(sr) + if err != ErrorAliasEmpty { + t.Errorf("expected %v, got %v", ErrorAliasEmpty, err) + } + + count, err := alias.DocCount() + if err != nil { + t.Errorf("error getting alias doc count: %v", err) + } + if count != 0 { + t.Errorf("expected %d, got %d", 0, count) + } +} + +func TestIndexAliasMulti(t *testing.T) { + score1, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(1.0), 0) + score2, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(2.0), 0) + ei1Count := uint64(7) + ei1 := &stubIndex{ + err: nil, + docCountResult: &ei1Count, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: search.DocumentMatchCollection{ + { + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 1.0, + }, + } + ei2Count := uint64(8) + ei2 := &stubIndex{ + err: nil, + docCountResult: &ei2Count, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: search.DocumentMatchCollection{ + { + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + }, + MaxScore: 2.0, + }, + } + + alias := NewIndexAlias(ei1, ei2) + + err := alias.Index("a", "a") + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + err = alias.Delete("a") + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + batch := alias.NewBatch() + err = alias.Batch(batch) + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + _, err = alias.Document("a") + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + _, err = alias.Fields() + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + _, err = alias.GetInternal([]byte("a")) + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + err = alias.SetInternal([]byte("a"), []byte("a")) + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + err = alias.DeleteInternal([]byte("a")) + if err != ErrorAliasMulti { + t.Errorf("expected %v, got %v", ErrorAliasMulti, err) + } + + mapping := alias.Mapping() + if mapping != nil { + t.Errorf("expected nil, got %v", mapping) + } + + indexStat := alias.Stats() + if indexStat != nil { + t.Errorf("expected nil, got %v", indexStat) + } + + // now a few things that should work + sr := NewSearchRequest(NewTermQuery("test")) + expected := &SearchResult{ + Status: &SearchStatus{ + Total: 2, + Successful: 2, + Errors: make(map[string]error), + }, + Total: 2, + Hits: search.DocumentMatchCollection{ + { + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + { + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 2.0, + } + results, err := alias.Search(sr) + if err != nil { + t.Error(err) + } + // cheat and ensure that Took field matches since it involves time + expected.Took = results.Took + if !reflect.DeepEqual(results, expected) { + t.Errorf("expected %#v, got %#v", expected, results) + } + + count, err := alias.DocCount() + if err != nil { + t.Errorf("error getting alias doc count: %v", err) + } + if count != (*ei1.docCountResult + *ei2.docCountResult) { + t.Errorf("expected %d, got %d", (*ei1.docCountResult + *ei2.docCountResult), count) + } +} + +// TestMultiSearchNoError +func TestMultiSearchNoError(t *testing.T) { + score1, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(1.0), 0) + score2, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(2.0), 0) + ei1 := &stubIndex{err: nil, searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: search.DocumentMatchCollection{ + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 1.0, + }} + ei2 := &stubIndex{err: nil, searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: search.DocumentMatchCollection{ + { + Index: "2", + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + }, + MaxScore: 2.0, + }} + + sr := NewSearchRequest(NewTermQuery("test")) + expected := &SearchResult{ + Status: &SearchStatus{ + Total: 2, + Successful: 2, + Errors: make(map[string]error), + }, + Total: 2, + Hits: search.DocumentMatchCollection{ + { + Index: "2", + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 2.0, + } + + results, err := MultiSearch(context.Background(), sr, nil, ei1, ei2) + if err != nil { + t.Error(err) + } + // cheat and ensure that Took field matches since it involves time + expected.Took = results.Took + if !reflect.DeepEqual(results, expected) { + t.Errorf("expected %#v, got %#v", expected, results) + } +} + +// TestMultiSearchSomeError +func TestMultiSearchSomeError(t *testing.T) { + ei1 := &stubIndex{name: "ei1", err: nil, searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: search.DocumentMatchCollection{ + { + ID: "a", + Score: 1.0, + }, + }, + Took: 1 * time.Second, + MaxScore: 1.0, + }} + ei2 := &stubIndex{name: "ei2", err: fmt.Errorf("deliberate error")} + sr := NewSearchRequest(NewTermQuery("test")) + res, err := MultiSearch(context.Background(), sr, nil, ei1, ei2) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if res.Status.Total != 2 { + t.Errorf("expected 2 indexes to be queried, got %d", res.Status.Total) + } + if res.Status.Failed != 1 { + t.Errorf("expected 1 index to fail, got %d", res.Status.Failed) + } + if res.Status.Successful != 1 { + t.Errorf("expected 1 index to be successful, got %d", res.Status.Successful) + } + if len(res.Status.Errors) != 1 { + t.Fatalf("expected 1 status error message, got %d", len(res.Status.Errors)) + } + if res.Status.Errors["ei2"].Error() != "deliberate error" { + t.Errorf("expected ei2 index error message 'deliberate error', got '%s'", res.Status.Errors["ei2"]) + } +} + +// TestMultiSearchAllError +// reproduces https://github.com/blevesearch/bleve/issues/126 +func TestMultiSearchAllError(t *testing.T) { + ei1 := &stubIndex{name: "ei1", err: fmt.Errorf("deliberate error")} + ei2 := &stubIndex{name: "ei2", err: fmt.Errorf("deliberate error")} + sr := NewSearchRequest(NewTermQuery("test")) + res, err := MultiSearch(context.Background(), sr, nil, ei1, ei2) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if res.Status.Total != 2 { + t.Errorf("expected 2 indexes to be queried, got %d", res.Status.Total) + } + if res.Status.Failed != 2 { + t.Errorf("expected 2 indexes to fail, got %d", res.Status.Failed) + } + if res.Status.Successful != 0 { + t.Errorf("expected 0 indexes to be successful, got %d", res.Status.Successful) + } + if len(res.Status.Errors) != 2 { + t.Fatalf("expected 2 status error messages, got %d", len(res.Status.Errors)) + } + if res.Status.Errors["ei1"].Error() != "deliberate error" { + t.Errorf("expected ei1 index error message 'deliberate error', got '%s'", res.Status.Errors["ei1"]) + } + if res.Status.Errors["ei2"].Error() != "deliberate error" { + t.Errorf("expected ei2 index error message 'deliberate error', got '%s'", res.Status.Errors["ei2"]) + } +} + +func TestMultiSearchSecondPage(t *testing.T) { + checkRequest := func(sr *SearchRequest) error { + if sr.From != 0 { + return fmt.Errorf("child request from should be 0") + } + if sr.Size != 20 { + return fmt.Errorf("child request size should be 20") + } + return nil + } + + ei1 := &stubIndex{ + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + }, + checkRequest: checkRequest, + } + ei2 := &stubIndex{ + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + }, + checkRequest: checkRequest, + } + sr := NewSearchRequestOptions(NewTermQuery("test"), 10, 10, false) + _, err := MultiSearch(context.Background(), sr, nil, ei1, ei2) + if err != nil { + t.Errorf("unexpected error %v", err) + } +} + +// TestMultiSearchTimeout tests simple timeout cases +// 1. all searches finish successfully before timeout +// 2. no searchers finish before the timeout +// 3. no searches finish before cancellation +func TestMultiSearchTimeout(t *testing.T) { + score1, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(1.0), 0) + score2, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(2.0), 0) + var ctx context.Context + ei1 := &stubIndex{ + name: "ei1", + checkRequest: func(req *SearchRequest) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(50 * time.Millisecond): + return nil + } + }, + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 1.0, + }, + } + ei2 := &stubIndex{ + name: "ei2", + checkRequest: func(req *SearchRequest) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(50 * time.Millisecond): + return nil + } + }, + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "2", + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + }, + MaxScore: 2.0, + }, + } + + // first run with absurdly long time out, should succeed + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + query := NewTermQuery("test") + sr := NewSearchRequest(query) + res, err := MultiSearch(ctx, sr, nil, ei1, ei2) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if res.Status.Total != 2 { + t.Errorf("expected 2 total, got %d", res.Status.Failed) + } + if res.Status.Successful != 2 { + t.Errorf("expected 0 success, got %d", res.Status.Successful) + } + if res.Status.Failed != 0 { + t.Errorf("expected 2 failed, got %d", res.Status.Failed) + } + if len(res.Status.Errors) != 0 { + t.Errorf("expected 0 errors, got %v", res.Status.Errors) + } + + // now run a search again with an absurdly low timeout (should timeout) + ctx, cancel = context.WithTimeout(context.Background(), 1*time.Microsecond) + defer cancel() + res, err = MultiSearch(ctx, sr, nil, ei1, ei2) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if res.Status.Total != 2 { + t.Errorf("expected 2 failed, got %d", res.Status.Failed) + } + if res.Status.Successful != 0 { + t.Errorf("expected 0 success, got %d", res.Status.Successful) + } + if res.Status.Failed != 2 { + t.Errorf("expected 2 failed, got %d", res.Status.Failed) + } + if len(res.Status.Errors) != 2 { + t.Errorf("expected 2 errors, got %v", res.Status.Errors) + } else { + if res.Status.Errors["ei1"].Error() != context.DeadlineExceeded.Error() { + t.Errorf("expected err for 'ei1' to be '%s' got '%s'", context.DeadlineExceeded.Error(), res.Status.Errors["ei1"]) + } + if res.Status.Errors["ei2"].Error() != context.DeadlineExceeded.Error() { + t.Errorf("expected err for 'ei2' to be '%s' got '%s'", context.DeadlineExceeded.Error(), res.Status.Errors["ei2"]) + } + } + + // now run a search again with a normal timeout, but cancel it first + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + cancel() + res, err = MultiSearch(ctx, sr, nil, ei1, ei2) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if res.Status.Total != 2 { + t.Errorf("expected 2 failed, got %d", res.Status.Failed) + } + if res.Status.Successful != 0 { + t.Errorf("expected 0 success, got %d", res.Status.Successful) + } + if res.Status.Failed != 2 { + t.Errorf("expected 2 failed, got %d", res.Status.Failed) + } + if len(res.Status.Errors) != 2 { + t.Errorf("expected 2 errors, got %v", res.Status.Errors) + } else { + if res.Status.Errors["ei1"].Error() != context.Canceled.Error() { + t.Errorf("expected err for 'ei1' to be '%s' got '%s'", context.Canceled.Error(), res.Status.Errors["ei1"]) + } + if res.Status.Errors["ei2"].Error() != context.Canceled.Error() { + t.Errorf("expected err for 'ei2' to be '%s' got '%s'", context.Canceled.Error(), res.Status.Errors["ei2"]) + } + } +} + +// TestMultiSearchTimeoutPartial tests the case where some indexes exceed +// the timeout, while others complete successfully +func TestMultiSearchTimeoutPartial(t *testing.T) { + score1, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(1.0), 0) + score2, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(2.0), 0) + score3, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(3.0), 0) + var ctx context.Context + ei1 := &stubIndex{ + name: "ei1", + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 1.0, + }, + } + ei2 := &stubIndex{ + name: "ei2", + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "2", + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + }, + MaxScore: 2.0, + }, + } + + ei3 := &stubIndex{ + name: "ei3", + checkRequest: func(req *SearchRequest) error { + <-ctx.Done() + return ctx.Err() + }, + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "3", + ID: "c", + Score: 3.0, + Sort: []string{string(score3)}, + }, + }, + MaxScore: 3.0, + }, + } + + // ei3 is set to take >50ms, so run search with timeout less than + // this, this should return partial results + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 25*time.Millisecond) + defer cancel() + query := NewTermQuery("test") + sr := NewSearchRequest(query) + expected := &SearchResult{ + Status: &SearchStatus{ + Total: 3, + Successful: 2, + Failed: 1, + Errors: map[string]error{ + "ei3": context.DeadlineExceeded, + }, + }, + Total: 2, + Hits: search.DocumentMatchCollection{ + { + Index: "2", + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 2.0, + } + + res, err := MultiSearch(ctx, sr, nil, ei1, ei2, ei3) + if err != nil { + t.Fatalf("expected no err, got %v", err) + } + expected.Took = res.Took + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected %#v, got %#v", expected, res) + } +} + +func TestIndexAliasMultipleLayer(t *testing.T) { + score1, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(1.0), 0) + score2, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(2.0), 0) + score3, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(3.0), 0) + score4, _ := numeric.NewPrefixCodedInt64(numeric.Float64ToInt64(4.0), 0) + var ctx context.Context + ei1 := &stubIndex{ + name: "ei1", + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 1.0, + }, + } + ei2 := &stubIndex{ + name: "ei2", + checkRequest: func(req *SearchRequest) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(250 * time.Millisecond): + return nil + } + }, + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "2", + ID: "b", + Score: 2.0, + Sort: []string{string(score2)}, + }, + }, + MaxScore: 2.0, + }, + } + + ei3 := &stubIndex{ + name: "ei3", + checkRequest: func(req *SearchRequest) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(250 * time.Millisecond): + return nil + } + }, + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "3", + ID: "c", + Score: 3.0, + Sort: []string{string(score3)}, + }, + }, + MaxScore: 3.0, + }, + } + + ei4 := &stubIndex{ + name: "ei4", + err: nil, + searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + Hits: []*search.DocumentMatch{ + { + Index: "4", + ID: "d", + Score: 4.0, + Sort: []string{string(score4)}, + }, + }, + MaxScore: 4.0, + }, + } + + alias1 := NewIndexAlias(ei1, ei2) + alias2 := NewIndexAlias(ei3, ei4) + aliasTop := NewIndexAlias(alias1, alias2) + + // ei2 and ei3 have 50ms delay + // search across aliasTop should still get results from ei1 and ei4 + // total should still be 4 + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 25*time.Millisecond) + defer cancel() + query := NewTermQuery("test") + sr := NewSearchRequest(query) + expected := &SearchResult{ + Status: &SearchStatus{ + Total: 4, + Successful: 2, + Failed: 2, + Errors: map[string]error{ + "ei2": context.DeadlineExceeded, + "ei3": context.DeadlineExceeded, + }, + }, + Total: 2, + Hits: search.DocumentMatchCollection{ + { + Index: "4", + ID: "d", + Score: 4.0, + Sort: []string{string(score4)}, + }, + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{string(score1)}, + }, + }, + MaxScore: 4.0, + } + + res, err := aliasTop.SearchInContext(ctx, sr) + if err != nil { + t.Fatalf("expected no err, got %v", err) + } + expected.Took = res.Took + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected %#v, got %#v", expected, res) + } +} + +// TestMultiSearchCustomSort +func TestMultiSearchCustomSort(t *testing.T) { + ei1 := &stubIndex{err: nil, searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 2, + Hits: search.DocumentMatchCollection{ + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{"albert"}, + }, + { + Index: "1", + ID: "b", + Score: 2.0, + Sort: []string{"crown"}, + }, + }, + MaxScore: 2.0, + }} + ei2 := &stubIndex{err: nil, searchResult: &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 2, + Hits: search.DocumentMatchCollection{ + { + Index: "2", + ID: "c", + Score: 2.5, + Sort: []string{"frank"}, + }, + { + Index: "2", + ID: "d", + Score: 3.0, + Sort: []string{"zombie"}, + }, + }, + MaxScore: 3.0, + }} + + sr := NewSearchRequest(NewTermQuery("test")) + sr.Explain = true + sr.SortBy([]string{"name"}) + expected := &SearchResult{ + Status: &SearchStatus{ + Total: 2, + Successful: 2, + Errors: make(map[string]error), + }, + Request: sr, + Total: 4, + Hits: search.DocumentMatchCollection{ + { + Index: "1", + ID: "a", + Score: 1.0, + Sort: []string{"albert"}, + }, + { + Index: "1", + ID: "b", + Score: 2.0, + Sort: []string{"crown"}, + }, + { + Index: "2", + ID: "c", + Score: 2.5, + Sort: []string{"frank"}, + }, + { + Index: "2", + ID: "d", + Score: 3.0, + Sort: []string{"zombie"}, + }, + }, + MaxScore: 3.0, + } + + results, err := MultiSearch(context.Background(), sr, nil, ei1, ei2) + if err != nil { + t.Error(err) + } + // cheat and ensure that Took field matches since it involves time + expected.Took = results.Took + if !reflect.DeepEqual(results, expected) { + t.Errorf("expected %v, got %v", expected, results) + } +} + +// stubIndex is an Index impl for which all operations +// return the configured error value, unless the +// corresponding operation result value has been +// set, in which case that is returned instead +type stubIndex struct { + name string + err error + searchResult *SearchResult + documentResult *document.Document + docCountResult *uint64 + checkRequest func(*SearchRequest) error +} + +func (i *stubIndex) Index(id string, data interface{}) error { + return i.err +} + +func (i *stubIndex) Delete(id string) error { + return i.err +} + +func (i *stubIndex) Batch(b *Batch) error { + return i.err +} + +func (i *stubIndex) Document(id string) (index.Document, error) { + if i.documentResult != nil { + return i.documentResult, nil + } + return nil, i.err +} + +func (i *stubIndex) DocCount() (uint64, error) { + if i.docCountResult != nil { + return *i.docCountResult, nil + } + return 0, i.err +} + +func (i *stubIndex) Search(req *SearchRequest) (*SearchResult, error) { + return i.SearchInContext(context.Background(), req) +} + +func (i *stubIndex) SearchInContext(ctx context.Context, req *SearchRequest) (*SearchResult, error) { + if i.checkRequest != nil { + err := i.checkRequest(req) + if err != nil { + return nil, err + } + } + if i.searchResult != nil { + return i.searchResult, nil + } + return nil, i.err +} + +func (i *stubIndex) Fields() ([]string, error) { + return nil, i.err +} + +func (i *stubIndex) FieldDict(field string) (index.FieldDict, error) { + return nil, i.err +} + +func (i *stubIndex) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) { + return nil, i.err +} + +func (i *stubIndex) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) { + return nil, i.err +} + +func (i *stubIndex) Close() error { + return i.err +} + +func (i *stubIndex) Mapping() mapping.IndexMapping { + return nil +} + +func (i *stubIndex) Stats() *IndexStat { + return nil +} + +func (i *stubIndex) StatsMap() map[string]interface{} { + return nil +} + +func (i *stubIndex) GetInternal(key []byte) ([]byte, error) { + return nil, i.err +} + +func (i *stubIndex) SetInternal(key, val []byte) error { + return i.err +} + +func (i *stubIndex) DeleteInternal(key []byte) error { + return i.err +} + +func (i *stubIndex) Advanced() (index.Index, error) { + return nil, nil +} + +func (i *stubIndex) NewBatch() *Batch { + return &Batch{} +} + +func (i *stubIndex) Name() string { + return i.name +} + +func (i *stubIndex) SetName(name string) { + i.name = name +} diff --git a/index_impl.go b/index_impl.go new file mode 100644 index 0000000..5cc0c58 --- /dev/null +++ b/index_impl.go @@ -0,0 +1,1288 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/microseconds" + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/milliseconds" + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/nanoseconds" + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/seconds" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/collector" + "github.com/blevesearch/bleve/v2/search/facet" + "github.com/blevesearch/bleve/v2/search/highlight" + "github.com/blevesearch/bleve/v2/search/query" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" + "github.com/blevesearch/geo/s2" +) + +type indexImpl struct { + path string + name string + meta *indexMeta + i index.Index + m mapping.IndexMapping + mutex sync.RWMutex + open bool + stats *IndexStat +} + +const storePath = "store" + +var mappingInternalKey = []byte("_mapping") + +const ( + SearchQueryStartCallbackKey search.ContextKey = "_search_query_start_callback_key" + SearchQueryEndCallbackKey search.ContextKey = "_search_query_end_callback_key" +) + +type ( + SearchQueryStartCallbackFn func(size uint64) error + SearchQueryEndCallbackFn func(size uint64) error +) + +func indexStorePath(path string) string { + return path + string(os.PathSeparator) + storePath +} + +func newIndexUsing(path string, mapping mapping.IndexMapping, indexType string, kvstore string, kvconfig map[string]interface{}) (*indexImpl, error) { + // first validate the mapping + err := mapping.Validate() + if err != nil { + return nil, err + } + + if kvconfig == nil { + kvconfig = map[string]interface{}{} + } + + if kvstore == "" { + return nil, fmt.Errorf("bleve not configured for file based indexing") + } + + rv := indexImpl{ + path: path, + name: path, + m: mapping, + meta: newIndexMeta(indexType, kvstore, kvconfig), + } + rv.stats = &IndexStat{i: &rv} + // at this point there is hope that we can be successful, so save index meta + if path != "" { + err = rv.meta.Save(path) + if err != nil { + return nil, err + } + kvconfig["create_if_missing"] = true + kvconfig["error_if_exists"] = true + kvconfig["path"] = indexStorePath(path) + } else { + kvconfig["path"] = "" + } + + // open the index + indexTypeConstructor := registry.IndexTypeConstructorByName(rv.meta.IndexType) + if indexTypeConstructor == nil { + return nil, ErrorUnknownIndexType + } + + rv.i, err = indexTypeConstructor(rv.meta.Storage, kvconfig, Config.analysisQueue) + if err != nil { + return nil, err + } + err = rv.i.Open() + if err != nil { + return nil, err + } + defer func(rv *indexImpl) { + if !rv.open { + rv.i.Close() + } + }(&rv) + + // now persist the mapping + mappingBytes, err := util.MarshalJSON(mapping) + if err != nil { + return nil, err + } + err = rv.i.SetInternal(mappingInternalKey, mappingBytes) + if err != nil { + return nil, err + } + + // mark the index as open + rv.mutex.Lock() + defer rv.mutex.Unlock() + rv.open = true + indexStats.Register(&rv) + return &rv, nil +} + +func openIndexUsing(path string, runtimeConfig map[string]interface{}) (rv *indexImpl, err error) { + rv = &indexImpl{ + path: path, + name: path, + } + rv.stats = &IndexStat{i: rv} + + rv.meta, err = openIndexMeta(path) + if err != nil { + return nil, err + } + + // backwards compatibility if index type is missing + if rv.meta.IndexType == "" { + rv.meta.IndexType = upsidedown.Name + } + + storeConfig := rv.meta.Config + if storeConfig == nil { + storeConfig = map[string]interface{}{} + } + + storeConfig["path"] = indexStorePath(path) + storeConfig["create_if_missing"] = false + storeConfig["error_if_exists"] = false + for rck, rcv := range runtimeConfig { + storeConfig[rck] = rcv + } + + // open the index + indexTypeConstructor := registry.IndexTypeConstructorByName(rv.meta.IndexType) + if indexTypeConstructor == nil { + return nil, ErrorUnknownIndexType + } + + rv.i, err = indexTypeConstructor(rv.meta.Storage, storeConfig, Config.analysisQueue) + if err != nil { + return nil, err + } + err = rv.i.Open() + if err != nil { + return nil, err + } + defer func(rv *indexImpl) { + if !rv.open { + rv.i.Close() + } + }(rv) + + // now load the mapping + indexReader, err := rv.i.Reader() + if err != nil { + return nil, err + } + defer func() { + if cerr := indexReader.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + mappingBytes, err := indexReader.GetInternal(mappingInternalKey) + if err != nil { + return nil, err + } + + var im *mapping.IndexMappingImpl + err = util.UnmarshalJSON(mappingBytes, &im) + if err != nil { + return nil, fmt.Errorf("error parsing mapping JSON: %v\nmapping contents:\n%s", err, string(mappingBytes)) + } + + // mark the index as open + rv.mutex.Lock() + defer rv.mutex.Unlock() + rv.open = true + + // validate the mapping + err = im.Validate() + if err != nil { + // note even if the mapping is invalid + // we still return an open usable index + return rv, err + } + + rv.m = im + indexStats.Register(rv) + return rv, err +} + +// Advanced returns internal index implementation +func (i *indexImpl) Advanced() (index.Index, error) { + return i.i, nil +} + +// Mapping returns the IndexMapping in use by this +// Index. +func (i *indexImpl) Mapping() mapping.IndexMapping { + return i.m +} + +// Index the object with the specified identifier. +// The IndexMapping for this index will determine +// how the object is indexed. +func (i *indexImpl) Index(id string, data interface{}) (err error) { + if id == "" { + return ErrorEmptyID + } + + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + i.FireIndexEvent() + + doc := document.NewDocument(id) + err = i.m.MapDocument(doc, data) + if err != nil { + return + } + err = i.i.Update(doc) + return +} + +// IndexSynonym indexes a synonym definition, with the specified id and belonging to the specified collection. +// Synonym definition defines term relationships for query expansion in searches. +func (i *indexImpl) IndexSynonym(id string, collection string, definition *SynonymDefinition) error { + if id == "" { + return ErrorEmptyID + } + + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + i.FireIndexEvent() + + synMap, ok := i.m.(mapping.SynonymMapping) + if !ok { + return ErrorSynonymSearchNotSupported + } + + if err := definition.Validate(); err != nil { + return err + } + + doc := document.NewSynonymDocument(id) + err := synMap.MapSynonymDocument(doc, collection, definition.Input, definition.Synonyms) + if err != nil { + return err + } + err = i.i.Update(doc) + return err +} + +// IndexAdvanced takes a document.Document object +// skips the mapping and indexes it. +func (i *indexImpl) IndexAdvanced(doc *document.Document) (err error) { + if doc.ID() == "" { + return ErrorEmptyID + } + + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err = i.i.Update(doc) + return +} + +// Delete entries for the specified identifier from +// the index. +func (i *indexImpl) Delete(id string) (err error) { + if id == "" { + return ErrorEmptyID + } + + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + err = i.i.Delete(id) + return +} + +// Batch executes multiple Index and Delete +// operations at the same time. There are often +// significant performance benefits when performing +// operations in a batch. +func (i *indexImpl) Batch(b *Batch) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + return i.i.Batch(b.internal) +} + +// Document is used to find the values of all the +// stored fields for a document in the index. These +// stored fields are put back into a Document object +// and returned. +func (i *indexImpl) Document(id string) (doc index.Document, err error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + indexReader, err := i.i.Reader() + if err != nil { + return nil, err + } + defer func() { + if cerr := indexReader.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + doc, err = indexReader.Document(id) + if err != nil { + return nil, err + } + return doc, nil +} + +// DocCount returns the number of documents in the +// index. +func (i *indexImpl) DocCount() (count uint64, err error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return 0, ErrorIndexClosed + } + + // open a reader for this search + indexReader, err := i.i.Reader() + if err != nil { + return 0, fmt.Errorf("error opening index reader %v", err) + } + defer func() { + if cerr := indexReader.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + count, err = indexReader.DocCount() + return +} + +// Search executes a search request operation. +// Returns a SearchResult object or an error. +func (i *indexImpl) Search(req *SearchRequest) (sr *SearchResult, err error) { + return i.SearchInContext(context.Background(), req) +} + +var ( + documentMatchEmptySize int + searchContextEmptySize int + facetResultEmptySize int + documentEmptySize int +) + +func init() { + var dm search.DocumentMatch + documentMatchEmptySize = dm.Size() + + var sc search.SearchContext + searchContextEmptySize = sc.Size() + + var fr search.FacetResult + facetResultEmptySize = fr.Size() + + var d document.Document + documentEmptySize = d.Size() +} + +// memNeededForSearch is a helper function that returns an estimate of RAM +// needed to execute a search request. +func memNeededForSearch(req *SearchRequest, + searcher search.Searcher, + topnCollector *collector.TopNCollector, +) uint64 { + backingSize := req.Size + req.From + 1 + if req.Size+req.From > collector.PreAllocSizeSkipCap { + backingSize = collector.PreAllocSizeSkipCap + 1 + } + numDocMatches := backingSize + searcher.DocumentMatchPoolSize() + + estimate := 0 + + // overhead, size in bytes from collector + estimate += topnCollector.Size() + + // pre-allocing DocumentMatchPool + estimate += searchContextEmptySize + numDocMatches*documentMatchEmptySize + + // searcher overhead + estimate += searcher.Size() + + // overhead from results, lowestMatchOutsideResults + estimate += (numDocMatches + 1) * documentMatchEmptySize + + // additional overhead from SearchResult + estimate += reflectStaticSizeSearchResult + reflectStaticSizeSearchStatus + + // overhead from facet results + if req.Facets != nil { + estimate += len(req.Facets) * facetResultEmptySize + } + + // highlighting, store + if len(req.Fields) > 0 || req.Highlight != nil { + // Size + From => number of hits + estimate += (req.Size + req.From) * documentEmptySize + } + + return uint64(estimate) +} + +func (i *indexImpl) preSearch(ctx context.Context, req *SearchRequest, reader index.IndexReader) (*SearchResult, error) { + var knnHits []*search.DocumentMatch + var err error + if requestHasKNN(req) { + knnHits, err = i.runKnnCollector(ctx, req, reader, true) + if err != nil { + return nil, err + } + } + + var fts search.FieldTermSynonymMap + var count uint64 + var fieldCardinality map[string]int + if !isMatchNoneQuery(req.Query) { + if synMap, ok := i.m.(mapping.SynonymMapping); ok { + if synReader, ok := reader.(index.ThesaurusReader); ok { + fts, err = query.ExtractSynonyms(ctx, synMap, synReader, req.Query, fts) + if err != nil { + return nil, err + } + } + } + if ok := isBM25Enabled(i.m); ok { + fieldCardinality = make(map[string]int) + count, err = reader.DocCount() + if err != nil { + return nil, err + } + + fs := make(query.FieldSet) + fs, err := query.ExtractFields(req.Query, i.m, fs) + if err != nil { + return nil, err + } + for field := range fs { + if bm25Reader, ok := reader.(index.BM25Reader); ok { + fieldCardinality[field], err = bm25Reader.FieldCardinality(field) + if err != nil { + return nil, err + } + } + } + } + } + + return &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + }, + Hits: knnHits, + SynonymResult: fts, + BM25Stats: &search.BM25Stats{ + DocCount: float64(count), + FieldCardinality: fieldCardinality, + }, + }, nil +} + +// SearchInContext executes a search request operation within the provided +// Context. Returns a SearchResult object or an error. +func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr *SearchResult, err error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + searchStart := time.Now() + + if !i.open { + return nil, ErrorIndexClosed + } + + // open a reader for this search + indexReader, err := i.i.Reader() + if err != nil { + return nil, fmt.Errorf("error opening index reader %v", err) + } + defer func() { + if cerr := indexReader.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + if _, ok := ctx.Value(search.PreSearchKey).(bool); ok { + preSearchResult, err := i.preSearch(ctx, req, indexReader) + if err != nil { + return nil, err + } + // increment the search count here itself, + // since the presearch may already satisfy + // the search request + atomic.AddUint64(&i.stats.searches, 1) + // increment the search time stat here as well, + // since presearch is part of the overall search + // operation and should be included in the search + // time stat + searchDuration := time.Since(searchStart) + atomic.AddUint64(&i.stats.searchTime, uint64(searchDuration)) + return preSearchResult, nil + } + + var reverseQueryExecution bool + if req.SearchBefore != nil { + reverseQueryExecution = true + req.Sort.Reverse() + req.SearchAfter = req.SearchBefore + req.SearchBefore = nil + } + + var coll *collector.TopNCollector + if req.SearchAfter != nil { + coll = collector.NewTopNCollectorAfter(req.Size, req.Sort, req.SearchAfter) + } else { + coll = collector.NewTopNCollector(req.Size, req.From, req.Sort) + } + + var knnHits []*search.DocumentMatch + var skipKNNCollector bool + + var fts search.FieldTermSynonymMap + var skipSynonymCollector bool + + var bm25Stats *search.BM25Stats + var ok bool + if req.PreSearchData != nil { + for k, v := range req.PreSearchData { + switch k { + case search.KnnPreSearchDataKey: + if v != nil { + knnHits, ok = v.([]*search.DocumentMatch) + if !ok { + return nil, fmt.Errorf("knn preSearchData must be of type []*search.DocumentMatch") + } + skipKNNCollector = true + } + case search.SynonymPreSearchDataKey: + if v != nil { + fts, ok = v.(search.FieldTermSynonymMap) + if !ok { + return nil, fmt.Errorf("synonym preSearchData must be of type search.FieldTermSynonymMap") + } + skipSynonymCollector = true + } + case search.BM25PreSearchDataKey: + if v != nil { + bm25Stats, ok = v.(*search.BM25Stats) + if !ok { + return nil, fmt.Errorf("bm25 preSearchData must be of type *search.BM25Stats") + } + } + } + } + } + if !skipKNNCollector && requestHasKNN(req) { + knnHits, err = i.runKnnCollector(ctx, req, indexReader, false) + if err != nil { + return nil, err + } + } + + if !skipSynonymCollector { + if synMap, ok := i.m.(mapping.SynonymMapping); ok && synMap.SynonymCount() > 0 { + if synReader, ok := indexReader.(index.ThesaurusReader); ok { + fts, err = query.ExtractSynonyms(ctx, synMap, synReader, req.Query, fts) + if err != nil { + return nil, err + } + } + } + } + + setKnnHitsInCollector(knnHits, req, coll) + + if fts != nil { + if is, ok := indexReader.(*scorch.IndexSnapshot); ok { + is.UpdateSynonymSearchCount(1) + } + ctx = context.WithValue(ctx, search.FieldTermSynonymMapKey, fts) + } + + scoringModelCallback := func() string { + if isBM25Enabled(i.m) { + return index.BM25Scoring + } + return index.DefaultScoringModel + } + ctx = context.WithValue(ctx, search.GetScoringModelCallbackKey, + search.GetScoringModelCallbackFn(scoringModelCallback)) + + // set the bm25Stats (stats important for consistent scoring) in + // the context object + if bm25Stats != nil { + ctx = context.WithValue(ctx, search.BM25StatsKey, bm25Stats) + } + + // This callback and variable handles the tracking of bytes read + // 1. as part of creation of tfr and its Next() calls which is + // accounted by invoking this callback when the TFR is closed. + // 2. the docvalues portion (accounted in collector) and the retrieval + // of stored fields bytes (by LoadAndHighlightFields) + var totalSearchCost uint64 + sendBytesRead := func(bytesRead uint64) { + totalSearchCost += bytesRead + } + + ctx = context.WithValue(ctx, search.SearchIOStatsCallbackKey, search.SearchIOStatsCallbackFunc(sendBytesRead)) + + var bufPool *s2.GeoBufferPool + getBufferPool := func() *s2.GeoBufferPool { + if bufPool == nil { + bufPool = s2.NewGeoBufferPool(search.MaxGeoBufPoolSize, search.MinGeoBufPoolSize) + } + + return bufPool + } + + ctx = context.WithValue(ctx, search.GeoBufferPoolCallbackKey, search.GeoBufferPoolCallbackFunc(getBufferPool)) + + searcher, err := req.Query.Searcher(ctx, indexReader, i.m, search.SearcherOptions{ + Explain: req.Explain, + IncludeTermVectors: req.IncludeLocations || req.Highlight != nil, + Score: req.Score, + }) + if err != nil { + return nil, err + } + defer func() { + if serr := searcher.Close(); err == nil && serr != nil { + err = serr + } + if sr != nil { + sr.Cost = totalSearchCost + } + if sr, ok := indexReader.(*scorch.IndexSnapshot); ok { + sr.UpdateIOStats(totalSearchCost) + } + + search.RecordSearchCost(ctx, search.DoneM, 0) + }() + + if req.Facets != nil { + facetsBuilder := search.NewFacetsBuilder(indexReader) + for facetName, facetRequest := range req.Facets { + if facetRequest.NumericRanges != nil { + // build numeric range facet + facetBuilder := facet.NewNumericFacetBuilder(facetRequest.Field, facetRequest.Size) + for _, nr := range facetRequest.NumericRanges { + facetBuilder.AddRange(nr.Name, nr.Min, nr.Max) + } + facetsBuilder.Add(facetName, facetBuilder) + } else if facetRequest.DateTimeRanges != nil { + // build date range facet + facetBuilder := facet.NewDateTimeFacetBuilder(facetRequest.Field, facetRequest.Size) + for _, dr := range facetRequest.DateTimeRanges { + dateTimeParserName := defaultDateTimeParser + if dr.DateTimeParser != "" { + dateTimeParserName = dr.DateTimeParser + } + dateTimeParser := i.m.DateTimeParserNamed(dateTimeParserName) + if dateTimeParser == nil { + return nil, fmt.Errorf("no date time parser named `%s` registered", dateTimeParserName) + } + start, end, err := dr.ParseDates(dateTimeParser) + if err != nil { + return nil, fmt.Errorf("ParseDates err: %v, using date time parser named %s", err, dateTimeParserName) + } + if start.IsZero() && end.IsZero() { + return nil, fmt.Errorf("date range query must specify either start, end or both for date range name '%s'", dr.Name) + } + facetBuilder.AddRange(dr.Name, start, end) + } + facetsBuilder.Add(facetName, facetBuilder) + } else { + // build terms facet + facetBuilder := facet.NewTermsFacetBuilder(facetRequest.Field, facetRequest.Size) + facetsBuilder.Add(facetName, facetBuilder) + } + } + coll.SetFacetsBuilder(facetsBuilder) + } + + memNeeded := memNeededForSearch(req, searcher, coll) + if cb := ctx.Value(SearchQueryStartCallbackKey); cb != nil { + if cbF, ok := cb.(SearchQueryStartCallbackFn); ok { + err = cbF(memNeeded) + } + } + if err != nil { + return nil, err + } + + if cb := ctx.Value(SearchQueryEndCallbackKey); cb != nil { + if cbF, ok := cb.(SearchQueryEndCallbackFn); ok { + defer func() { + _ = cbF(memNeeded) + }() + } + } + + err = coll.Collect(ctx, searcher, indexReader) + if err != nil { + return nil, err + } + + hits := coll.Results() + + var highlighter highlight.Highlighter + + if req.Highlight != nil { + // get the right highlighter + highlighter, err = Config.Cache.HighlighterNamed(Config.DefaultHighlighter) + if err != nil { + return nil, err + } + if req.Highlight.Style != nil { + highlighter, err = Config.Cache.HighlighterNamed(*req.Highlight.Style) + if err != nil { + return nil, err + } + } + if highlighter == nil { + return nil, fmt.Errorf("no highlighter named `%s` registered", *req.Highlight.Style) + } + } + + var storedFieldsCost uint64 + for _, hit := range hits { + // KNN documents will already have their Index value set as part of the knn collector output + // so check if the index is empty and set it to the current index name + if i.name != "" && hit.Index == "" { + hit.Index = i.name + } + err, storedFieldsBytes := LoadAndHighlightFields(hit, req, i.name, indexReader, highlighter) + if err != nil { + return nil, err + } + storedFieldsCost += storedFieldsBytes + } + + totalSearchCost += storedFieldsCost + search.RecordSearchCost(ctx, search.AddM, storedFieldsCost) + + if req.PreSearchData == nil { + // increment the search count only if this is not a second-phase search + // (e.g., for Hybrid Search), since the first-phase search already increments it + atomic.AddUint64(&i.stats.searches, 1) + } + // increment the search time stat, as the first-phase search is part of + // the overall operation; adding second-phase time later keeps it accurate + searchDuration := time.Since(searchStart) + atomic.AddUint64(&i.stats.searchTime, uint64(searchDuration)) + + if Config.SlowSearchLogThreshold > 0 && + searchDuration > Config.SlowSearchLogThreshold { + logger.Printf("slow search took %s - %v", searchDuration, req) + } + + if reverseQueryExecution { + // reverse the sort back to the original + req.Sort.Reverse() + // resort using the original order + mhs := newSearchHitSorter(req.Sort, hits) + req.SortFunc()(mhs) + // reset request + req.SearchBefore = req.SearchAfter + req.SearchAfter = nil + } + + rv := &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + }, + Hits: hits, + Total: coll.Total(), + MaxScore: coll.MaxScore(), + Took: searchDuration, + Facets: coll.FacetResults(), + } + + if req.Explain { + rv.Request = req + } + + return rv, nil +} + +func LoadAndHighlightFields(hit *search.DocumentMatch, req *SearchRequest, + indexName string, r index.IndexReader, + highlighter highlight.Highlighter, +) (error, uint64) { + var totalStoredFieldsBytes uint64 + if len(req.Fields) > 0 || highlighter != nil { + doc, err := r.Document(hit.ID) + if err == nil && doc != nil { + if len(req.Fields) > 0 && hit.Fields == nil { + totalStoredFieldsBytes = doc.StoredFieldsBytes() + fieldsToLoad := deDuplicate(req.Fields) + for _, f := range fieldsToLoad { + doc.VisitFields(func(docF index.Field) { + if f == "*" || docF.Name() == f { + var value interface{} + switch docF := docF.(type) { + case index.TextField: + value = docF.Text() + case index.NumericField: + num, err := docF.Number() + if err == nil { + value = num + } + case index.DateTimeField: + datetime, layout, err := docF.DateTime() + if err == nil { + if layout == "" { + // missing layout means we fallback to + // the default layout which is RFC3339 + value = datetime.Format(time.RFC3339) + } else { + // the layout here can now either be representative + // of an actual datetime layout or a timestamp + switch layout { + case seconds.Name: + value = strconv.FormatInt(datetime.Unix(), 10) + case milliseconds.Name: + value = strconv.FormatInt(datetime.UnixMilli(), 10) + case microseconds.Name: + value = strconv.FormatInt(datetime.UnixMicro(), 10) + case nanoseconds.Name: + value = strconv.FormatInt(datetime.UnixNano(), 10) + default: + // the layout for formatting the date to a string + // is provided by a datetime parser which is not + // handling the timestamp case, hence the layout + // can be directly used to format the date + value = datetime.Format(layout) + } + } + } + case index.BooleanField: + boolean, err := docF.Boolean() + if err == nil { + value = boolean + } + case index.GeoPointField: + lon, err := docF.Lon() + if err == nil { + lat, err := docF.Lat() + if err == nil { + value = []float64{lon, lat} + } + } + case index.GeoShapeField: + v, err := docF.GeoShape() + if err == nil { + value = v + } + case index.IPField: + ip, err := docF.IP() + if err == nil { + value = ip.String() + } + } + + if value != nil { + hit.AddFieldValue(docF.Name(), value) + } + } + }) + } + } + if highlighter != nil { + highlightFields := req.Highlight.Fields + if highlightFields == nil { + // add all fields with matches + highlightFields = make([]string, 0, len(hit.Locations)) + for k := range hit.Locations { + highlightFields = append(highlightFields, k) + } + } + for _, hf := range highlightFields { + highlighter.BestFragmentsInField(hit, doc, hf, 1) + } + } + } else if doc == nil { + // unexpected case, a doc ID that was found as a search hit + // was unable to be found during document lookup + return ErrorIndexReadInconsistency, 0 + } + } + + return nil, totalStoredFieldsBytes +} + +// Fields returns the name of all the fields this +// Index has operated on. +func (i *indexImpl) Fields() (fields []string, err error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + + indexReader, err := i.i.Reader() + if err != nil { + return nil, err + } + defer func() { + if cerr := indexReader.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + fields, err = indexReader.Fields() + if err != nil { + return nil, err + } + return fields, nil +} + +func (i *indexImpl) FieldDict(field string) (index.FieldDict, error) { + i.mutex.RLock() + + if !i.open { + i.mutex.RUnlock() + return nil, ErrorIndexClosed + } + + indexReader, err := i.i.Reader() + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + fieldDict, err := indexReader.FieldDict(field) + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + return &indexImplFieldDict{ + index: i, + indexReader: indexReader, + fieldDict: fieldDict, + }, nil +} + +func (i *indexImpl) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) { + i.mutex.RLock() + + if !i.open { + i.mutex.RUnlock() + return nil, ErrorIndexClosed + } + + indexReader, err := i.i.Reader() + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + fieldDict, err := indexReader.FieldDictRange(field, startTerm, endTerm) + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + return &indexImplFieldDict{ + index: i, + indexReader: indexReader, + fieldDict: fieldDict, + }, nil +} + +func (i *indexImpl) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) { + i.mutex.RLock() + + if !i.open { + i.mutex.RUnlock() + return nil, ErrorIndexClosed + } + + indexReader, err := i.i.Reader() + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + fieldDict, err := indexReader.FieldDictPrefix(field, termPrefix) + if err != nil { + i.mutex.RUnlock() + return nil, err + } + + return &indexImplFieldDict{ + index: i, + indexReader: indexReader, + fieldDict: fieldDict, + }, nil +} + +func (i *indexImpl) Close() error { + i.mutex.Lock() + defer i.mutex.Unlock() + + indexStats.UnRegister(i) + + i.open = false + return i.i.Close() +} + +func (i *indexImpl) Stats() *IndexStat { + return i.stats +} + +func (i *indexImpl) StatsMap() map[string]interface{} { + return i.stats.statsMap() +} + +func (i *indexImpl) GetInternal(key []byte) (val []byte, err error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return nil, ErrorIndexClosed + } + + reader, err := i.i.Reader() + if err != nil { + return nil, err + } + defer func() { + if cerr := reader.Close(); err == nil && cerr != nil { + err = cerr + } + }() + + val, err = reader.GetInternal(key) + if err != nil { + return nil, err + } + return val, nil +} + +func (i *indexImpl) SetInternal(key, val []byte) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + return i.i.SetInternal(key, val) +} + +func (i *indexImpl) DeleteInternal(key []byte) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + return i.i.DeleteInternal(key) +} + +// NewBatch creates a new empty batch. +func (i *indexImpl) NewBatch() *Batch { + return &Batch{ + index: i, + internal: index.NewBatch(), + } +} + +func (i *indexImpl) Name() string { + return i.name +} + +func (i *indexImpl) SetName(name string) { + indexStats.UnRegister(i) + i.name = name + indexStats.Register(i) +} + +type indexImplFieldDict struct { + index *indexImpl + indexReader index.IndexReader + fieldDict index.FieldDict +} + +func (f *indexImplFieldDict) BytesRead() uint64 { + return f.fieldDict.BytesRead() +} + +func (f *indexImplFieldDict) Next() (*index.DictEntry, error) { + return f.fieldDict.Next() +} + +func (f *indexImplFieldDict) Close() error { + defer f.index.mutex.RUnlock() + err := f.fieldDict.Close() + if err != nil { + return err + } + return f.indexReader.Close() +} + +func (f *indexImplFieldDict) Cardinality() int { + return f.fieldDict.Cardinality() +} + +// helper function to remove duplicate entries from slice of strings +func deDuplicate(fields []string) []string { + entries := make(map[string]struct{}) + ret := []string{} + for _, entry := range fields { + if _, exists := entries[entry]; !exists { + entries[entry] = struct{}{} + ret = append(ret, entry) + } + } + return ret +} + +type searchHitSorter struct { + hits search.DocumentMatchCollection + sort search.SortOrder + cachedScoring []bool + cachedDesc []bool +} + +func newSearchHitSorter(sort search.SortOrder, hits search.DocumentMatchCollection) *searchHitSorter { + return &searchHitSorter{ + sort: sort, + hits: hits, + cachedScoring: sort.CacheIsScore(), + cachedDesc: sort.CacheDescending(), + } +} + +func (m *searchHitSorter) Len() int { return len(m.hits) } +func (m *searchHitSorter) Swap(i, j int) { m.hits[i], m.hits[j] = m.hits[j], m.hits[i] } +func (m *searchHitSorter) Less(i, j int) bool { + c := m.sort.Compare(m.cachedScoring, m.cachedDesc, m.hits[i], m.hits[j]) + return c < 0 +} + +func (i *indexImpl) CopyTo(d index.Directory) (err error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if !i.open { + return ErrorIndexClosed + } + + copyIndex, ok := i.i.(index.CopyIndex) + if !ok { + return fmt.Errorf("index implementation does not support copy reader") + } + + copyReader := copyIndex.CopyReader() + if copyReader == nil { + return fmt.Errorf("index's copyReader is nil") + } + + defer func() { + if cerr := copyReader.CloseCopyReader(); err == nil && cerr != nil { + err = cerr + } + }() + + err = copyReader.CopyTo(d) + if err != nil { + return fmt.Errorf("error copying index metadata: %v", err) + } + + // copy the metadata + return i.meta.CopyTo(d) +} + +func (f FileSystemDirectory) GetWriter(filePath string) (io.WriteCloser, + error, +) { + dir, file := filepath.Split(filePath) + if dir != "" { + err := os.MkdirAll(filepath.Join(string(f), dir), os.ModePerm) + if err != nil { + return nil, err + } + } + + return os.OpenFile(filepath.Join(string(f), dir, file), + os.O_RDWR|os.O_CREATE, 0o600) +} + +func (i *indexImpl) FireIndexEvent() { + // get the internal index implementation + internalIndex, err := i.Advanced() + if err != nil { + return + } + // check if the internal index implementation supports events + if internalEventIndex, ok := internalIndex.(index.EventIndex); ok { + // fire the Index() event + internalEventIndex.FireIndexEvent() + } +} diff --git a/index_meta.go b/index_meta.go new file mode 100644 index 0000000..14b88dc --- /dev/null +++ b/index_meta.go @@ -0,0 +1,115 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +const metaFilename = "index_meta.json" + +type indexMeta struct { + Storage string `json:"storage"` + IndexType string `json:"index_type"` + Config map[string]interface{} `json:"config,omitempty"` +} + +func newIndexMeta(indexType string, storage string, config map[string]interface{}) *indexMeta { + return &indexMeta{ + IndexType: indexType, + Storage: storage, + Config: config, + } +} + +func openIndexMeta(path string) (*indexMeta, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, ErrorIndexPathDoesNotExist + } + indexMetaPath := indexMetaPath(path) + metaBytes, err := os.ReadFile(indexMetaPath) + if err != nil { + return nil, ErrorIndexMetaMissing + } + var im indexMeta + err = util.UnmarshalJSON(metaBytes, &im) + if err != nil { + return nil, ErrorIndexMetaCorrupt + } + if im.IndexType == "" { + im.IndexType = upsidedown.Name + } + return &im, nil +} + +func (i *indexMeta) Save(path string) (err error) { + indexMetaPath := indexMetaPath(path) + // ensure any necessary parent directories exist + err = os.MkdirAll(path, 0700) + if err != nil { + if os.IsExist(err) { + return ErrorIndexPathExists + } + return err + } + metaBytes, err := util.MarshalJSON(i) + if err != nil { + return err + } + indexMetaFile, err := os.OpenFile(indexMetaPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + if err != nil { + if os.IsExist(err) { + return ErrorIndexPathExists + } + return err + } + defer func() { + if ierr := indexMetaFile.Close(); err == nil && ierr != nil { + err = ierr + } + }() + _, err = indexMetaFile.Write(metaBytes) + if err != nil { + return err + } + return nil +} + +func (i *indexMeta) CopyTo(d index.Directory) (err error) { + metaBytes, err := util.MarshalJSON(i) + if err != nil { + return err + } + + w, err := d.GetWriter(metaFilename) + if w == nil || err != nil { + return fmt.Errorf("invalid writer for file: %s, err: %v", + metaFilename, err) + } + defer w.Close() + + _, err = w.Write(metaBytes) + return err +} + +func indexMetaPath(path string) string { + return filepath.Join(path, metaFilename) +} diff --git a/index_meta_test.go b/index_meta_test.go new file mode 100644 index 0000000..7719f57 --- /dev/null +++ b/index_meta_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "os" + "testing" +) + +func TestIndexMeta(t *testing.T) { + var testIndexPath = "doesnotexit.bleve" + defer func() { + err := os.RemoveAll(testIndexPath) + if err != nil { + t.Fatal(err) + } + }() + + // open non-existent meta should give an error + _, err := openIndexMeta(testIndexPath) + if err == nil { + t.Errorf("expected error, got nil") + } + + // create meta + im := &indexMeta{Storage: "boltdb"} + err = im.Save(testIndexPath) + if err != nil { + t.Error(err) + } + im = nil + + // open a meta that exists + im, err = openIndexMeta(testIndexPath) + if err != nil { + t.Error(err) + } + if im.Storage != "boltdb" { + t.Errorf("expected storage 'boltdb', got '%s'", im.Storage) + } + + // save a meta that already exists + err = im.Save(testIndexPath) + if err == nil { + t.Errorf("expected error, got nil") + } +} diff --git a/index_stats.go b/index_stats.go new file mode 100644 index 0000000..2d303f6 --- /dev/null +++ b/index_stats.go @@ -0,0 +1,75 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "encoding/json" + "sync" + "sync/atomic" +) + +type IndexStat struct { + searches uint64 + searchTime uint64 + i *indexImpl +} + +func (is *IndexStat) statsMap() map[string]interface{} { + m := map[string]interface{}{} + m["index"] = is.i.i.StatsMap() + m["searches"] = atomic.LoadUint64(&is.searches) + m["search_time"] = atomic.LoadUint64(&is.searchTime) + return m +} + +func (is *IndexStat) MarshalJSON() ([]byte, error) { + m := is.statsMap() + return json.Marshal(m) +} + +type IndexStats struct { + indexes map[string]*IndexStat + mutex sync.RWMutex +} + +func NewIndexStats() *IndexStats { + return &IndexStats{ + indexes: make(map[string]*IndexStat), + } +} + +func (i *IndexStats) Register(index Index) { + i.mutex.Lock() + defer i.mutex.Unlock() + i.indexes[index.Name()] = index.Stats() +} + +func (i *IndexStats) UnRegister(index Index) { + i.mutex.Lock() + defer i.mutex.Unlock() + delete(i.indexes, index.Name()) +} + +func (i *IndexStats) String() string { + i.mutex.RLock() + defer i.mutex.RUnlock() + bytes, err := json.Marshal(i.indexes) + if err != nil { + return "error marshaling stats" + } + return string(bytes) +} + +var indexStats *IndexStats diff --git a/index_test.go b/index_test.go new file mode 100644 index 0000000..b0d0d8d --- /dev/null +++ b/index_test.go @@ -0,0 +1,3247 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "math" + "os" + "path/filepath" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/null" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/query" + index "github.com/blevesearch/bleve_index_api" + + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown" +) + +type Fatalfable interface { + Fatalf(format string, args ...interface{}) +} + +func createTmpIndexPath(f Fatalfable) string { + tmpIndexPath, err := os.MkdirTemp("", "bleve-testidx") + if err != nil { + f.Fatalf("error creating temp dir: %v", err) + } + return tmpIndexPath +} + +func cleanupTmpIndexPath(f Fatalfable, path string) { + err := os.RemoveAll(path) + if err != nil { + f.Fatalf("error removing temp dir: %v", err) + } +} + +func TestCrud(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + doca := map[string]interface{}{ + "name": "marty", + "desc": "gophercon india", + } + err = idx.Index("a", doca) + if err != nil { + t.Error(err) + } + + docy := map[string]interface{}{ + "name": "jasper", + "desc": "clojure", + } + err = idx.Index("y", docy) + if err != nil { + t.Error(err) + } + + err = idx.Delete("y") + if err != nil { + t.Error(err) + } + + docx := map[string]interface{}{ + "name": "rose", + "desc": "googler", + } + err = idx.Index("x", docx) + if err != nil { + t.Error(err) + } + + err = idx.SetInternal([]byte("status"), []byte("pending")) + if err != nil { + t.Error(err) + } + + docb := map[string]interface{}{ + "name": "steve", + "desc": "cbft master", + } + batch := idx.NewBatch() + err = batch.Index("b", docb) + if err != nil { + t.Error(err) + } + batch.Delete("x") + batch.SetInternal([]byte("batchi"), []byte("batchv")) + batch.DeleteInternal([]byte("status")) + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + val, err := idx.GetInternal([]byte("batchi")) + if err != nil { + t.Error(err) + } + if string(val) != "batchv" { + t.Errorf("expected 'batchv', got '%s'", val) + } + val, err = idx.GetInternal([]byte("status")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got '%s'", val) + } + + err = idx.SetInternal([]byte("seqno"), []byte("7")) + if err != nil { + t.Error(err) + } + err = idx.SetInternal([]byte("status"), []byte("ready")) + if err != nil { + t.Error(err) + } + err = idx.DeleteInternal([]byte("status")) + if err != nil { + t.Error(err) + } + val, err = idx.GetInternal([]byte("status")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got '%s'", val) + } + + val, err = idx.GetInternal([]byte("seqno")) + if err != nil { + t.Error(err) + } + if string(val) != "7" { + t.Errorf("expected '7', got '%s'", val) + } + + // close the index, open it again, and try some more things + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + idx, err = Open(tmpIndexPath) + if err != nil { + t.Fatal(err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + count, err := idx.DocCount() + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Errorf("expected doc count 2, got %d", count) + } + + doc, err := idx.Document("a") + if err != nil { + t.Fatal(err) + } + foundNameField := false + doc.VisitFields(func(field index.Field) { + if field.Name() == "name" && string(field.Value()) == "marty" { + foundNameField = true + } + }) + if !foundNameField { + t.Errorf("expected to find field named 'name' with value 'marty'") + } + + fields, err := idx.Fields() + if err != nil { + t.Fatal(err) + } + expectedFields := map[string]bool{ + "_all": false, + "name": false, + "desc": false, + } + if len(fields) < len(expectedFields) { + t.Fatalf("expected %d fields got %d", len(expectedFields), len(fields)) + } + for _, f := range fields { + expectedFields[f] = true + } + for ef, efp := range expectedFields { + if !efp { + t.Errorf("field %s is missing", ef) + } + } +} + +func approxSame(actual, expected uint64) bool { + modulus := func(a, b uint64) uint64 { + if a > b { + return a - b + } + return b - a + } + + return float64(modulus(actual, expected))/float64(expected) < float64(0.30) +} + +func checkStatsOnIndexedBatch(indexPath string, indexMapping mapping.IndexMapping, + expectedVal uint64, +) error { + var wg sync.WaitGroup + var statValError error + + idx, err := NewUsing(indexPath, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + return err + } + + batch, err := getBatchFromData(idx, "sample-data.json") + if err != nil { + return fmt.Errorf("failed to form a batch %v\n", err) + } + wg.Add(1) + batch.SetPersistedCallback(func(e error) { + defer wg.Done() + stats, _ := idx.StatsMap()["index"].(map[string]interface{}) + bytesWritten, _ := stats["num_bytes_written_at_index_time"].(uint64) + if !approxSame(bytesWritten, expectedVal) { + statValError = fmt.Errorf("expected bytes written is %d, got %v", expectedVal, + bytesWritten) + } + }) + err = idx.Batch(batch) + if err != nil { + return fmt.Errorf("failed to index batch %v\n", err) + } + wg.Wait() + idx.Close() + + return statValError +} + +func TestBytesWritten(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("hotel", documentMapping) + + indexMapping.DocValuesDynamic = false + indexMapping.StoreDynamic = false + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Index = true + contentFieldMapping.Store = false + contentFieldMapping.IncludeInAll = false + contentFieldMapping.IncludeTermVectors = false + contentFieldMapping.DocValues = false + + reviewsMapping := NewDocumentMapping() + reviewsMapping.AddFieldMappingsAt("content", contentFieldMapping) + documentMapping.AddSubDocumentMapping("reviews", reviewsMapping) + + typeFieldMapping := NewTextFieldMapping() + typeFieldMapping.Store = false + typeFieldMapping.IncludeInAll = false + typeFieldMapping.IncludeTermVectors = false + typeFieldMapping.DocValues = false + documentMapping.AddFieldMappingsAt("type", typeFieldMapping) + + err = checkStatsOnIndexedBatch(tmpIndexPath, indexMapping, 57273) + if err != nil { + t.Fatal(err) + } + cleanupTmpIndexPath(t, tmpIndexPath) + + contentFieldMapping.Store = true + tmpIndexPath1 := createTmpIndexPath(t) + + err := checkStatsOnIndexedBatch(tmpIndexPath1, indexMapping, 76069) + if err != nil { + t.Fatal(err) + } + cleanupTmpIndexPath(t, tmpIndexPath1) + + contentFieldMapping.Store = false + contentFieldMapping.IncludeInAll = true + tmpIndexPath2 := createTmpIndexPath(t) + + err = checkStatsOnIndexedBatch(tmpIndexPath2, indexMapping, 68875) + if err != nil { + t.Fatal(err) + } + cleanupTmpIndexPath(t, tmpIndexPath2) + + contentFieldMapping.IncludeInAll = false + contentFieldMapping.IncludeTermVectors = true + tmpIndexPath3 := createTmpIndexPath(t) + + err = checkStatsOnIndexedBatch(tmpIndexPath3, indexMapping, 78985) + if err != nil { + t.Fatal(err) + } + cleanupTmpIndexPath(t, tmpIndexPath3) + + contentFieldMapping.IncludeTermVectors = false + contentFieldMapping.DocValues = true + tmpIndexPath4 := createTmpIndexPath(t) + + err = checkStatsOnIndexedBatch(tmpIndexPath4, indexMapping, 64228) + if err != nil { + t.Fatal(err) + } + cleanupTmpIndexPath(t, tmpIndexPath4) +} + +func createIndexMappingOnSampleData() *mapping.IndexMappingImpl { + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + indexMapping.ScoringModel = index.DefaultScoringModel + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("hotel", documentMapping) + indexMapping.StoreDynamic = false + indexMapping.DocValuesDynamic = false + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Store = false + + reviewsMapping := NewDocumentMapping() + reviewsMapping.AddFieldMappingsAt("content", contentFieldMapping) + documentMapping.AddSubDocumentMapping("reviews", reviewsMapping) + + typeFieldMapping := NewTextFieldMapping() + typeFieldMapping.Store = false + documentMapping.AddFieldMappingsAt("type", typeFieldMapping) + + return indexMapping +} + +func TestBM25TFIDFScoring(t *testing.T) { + tmpIndexPath1 := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath1) + tmpIndexPath2 := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath2) + + indexMapping := createIndexMappingOnSampleData() + indexMapping.ScoringModel = index.BM25Scoring + indexBM25, err := NewUsing(tmpIndexPath1, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + + indexMapping1 := createIndexMappingOnSampleData() + indexTFIDF, err := NewUsing(tmpIndexPath2, indexMapping1, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := indexBM25.Close() + if err != nil { + t.Fatal(err) + } + + err = indexTFIDF.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch, err := getBatchFromData(indexBM25, "sample-data.json") + if err != nil { + t.Fatalf("failed to form a batch") + } + err = indexBM25.Batch(batch) + if err != nil { + t.Fatalf("failed to index batch %v\n", err) + } + query := NewMatchQuery("Hotel") + query.FieldVal = "name" + searchRequest := NewSearchRequestOptions(query, int(10), 0, true) + + resBM25, err := indexBM25.Search(searchRequest) + if err != nil { + t.Error(err) + } + + batch, err = getBatchFromData(indexTFIDF, "sample-data.json") + if err != nil { + t.Fatalf("failed to form a batch") + } + err = indexTFIDF.Batch(batch) + if err != nil { + t.Fatalf("failed to index batch %v\n", err) + } + + resTFIDF, err := indexTFIDF.Search(searchRequest) + if err != nil { + t.Error(err) + } + + for i, hit := range resTFIDF.Hits { + if hit.Score < resBM25.Hits[i].Score { + t.Fatalf("expected the score to be higher for BM25, got %v and %v", + resBM25.Hits[i].Score, hit.Score) + } + } +} + +func TestBM25GlobalScoring(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + indexMapping := createIndexMappingOnSampleData() + indexMapping.ScoringModel = index.BM25Scoring + idxSinglePartition, err := NewUsing(tmpIndexPath, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := idxSinglePartition.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch, err := getBatchFromData(idxSinglePartition, "sample-data.json") + if err != nil { + t.Fatalf("failed to form a batch") + } + err = idxSinglePartition.Batch(batch) + if err != nil { + t.Fatalf("failed to index batch %v\n", err) + } + query := NewMatchQuery("Hotel") + query.FieldVal = "name" + searchRequest := NewSearchRequestOptions(query, int(10), 0, true) + + res, err := idxSinglePartition.Search(searchRequest) + if err != nil { + t.Error(err) + } + + singlePartHits := res.Hits + + dataset, _ := readDataFromFile("sample-data.json") + tmpIndexPath1 := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath1) + + idxPart1, err := NewUsing(tmpIndexPath1, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := idxPart1.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch1 := idxPart1.NewBatch() + for _, doc := range dataset[:len(dataset)/2] { + err = batch1.Index(fmt.Sprintf("%d", doc["id"]), doc) + if err != nil { + t.Fatal(err) + } + } + err = idxPart1.Batch(batch1) + if err != nil { + t.Fatal(err) + } + + tmpIndexPath2 := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath2) + + idxPart2, err := NewUsing(tmpIndexPath2, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := idxPart2.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch2 := idxPart2.NewBatch() + for _, doc := range dataset[len(dataset)/2:] { + err = batch2.Index(fmt.Sprintf("%d", doc["id"]), doc) + if err != nil { + t.Fatal(err) + } + } + err = idxPart2.Batch(batch2) + if err != nil { + t.Fatal(err) + } + + multiPartIndex := NewIndexAlias(idxPart1, idxPart2) + err = multiPartIndex.SetIndexMapping(indexMapping) + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + // this key is set to ensure that we have a consistent scoring at the index alias + // level (it forces a pre search phase which can have a small overhead) + ctx = context.WithValue(ctx, search.SearchTypeKey, search.GlobalScoring) + + res, err = multiPartIndex.SearchInContext(ctx, searchRequest) + if err != nil { + t.Error(err) + } + + for i, hit := range res.Hits { + if hit.Score != singlePartHits[i].Score { + t.Fatalf("expected the scores to be the same, got %v and %v", + hit.Score, singlePartHits[i].Score) + } + } +} + +func TestBytesRead(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("hotel", documentMapping) + indexMapping.StoreDynamic = false + indexMapping.DocValuesDynamic = false + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Store = false + + reviewsMapping := NewDocumentMapping() + reviewsMapping.AddFieldMappingsAt("content", contentFieldMapping) + documentMapping.AddSubDocumentMapping("reviews", reviewsMapping) + + typeFieldMapping := NewTextFieldMapping() + typeFieldMapping.Store = false + documentMapping.AddFieldMappingsAt("type", typeFieldMapping) + + idx, err := NewUsing(tmpIndexPath, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch, err := getBatchFromData(idx, "sample-data.json") + if err != nil { + t.Fatalf("failed to form a batch") + } + err = idx.Batch(batch) + if err != nil { + t.Fatalf("failed to index batch %v\n", err) + } + query := NewQueryStringQuery("united") + searchRequest := NewSearchRequestOptions(query, int(10), 0, true) + + res, err := idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + stats, _ := idx.StatsMap()["index"].(map[string]interface{}) + prevBytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64) + + expectedBytesRead := uint64(22049) + if supportForVectorSearch { + expectedBytesRead = 22459 + } + + if prevBytesRead != expectedBytesRead && res.Cost == prevBytesRead { + t.Fatalf("expected bytes read for query string %v, got %v", + expectedBytesRead, prevBytesRead) + } + + // subsequent queries on the same field results in lesser amount + // of bytes read because the segment static and dictionary is reused and not + // loaded from mmap'd filed + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 66 && res.Cost == bytesRead-prevBytesRead { + t.Fatalf("expected bytes read for query string 66, got %v", + bytesRead-prevBytesRead) + } + prevBytesRead = bytesRead + + fuzz := NewFuzzyQuery("hotel") + fuzz.FieldVal = "reviews.content" + fuzz.Fuzziness = 2 + searchRequest = NewSearchRequest(fuzz) + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 8468 && res.Cost == bytesRead-prevBytesRead { + t.Fatalf("expected bytes read for fuzzy query is 8468, got %v", + bytesRead-prevBytesRead) + } + prevBytesRead = bytesRead + + typeFacet := NewFacetRequest("type", 2) + query = NewQueryStringQuery("united") + searchRequest = NewSearchRequestOptions(query, int(0), 0, true) + searchRequest.AddFacet("types", typeFacet) + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if !approxSame(bytesRead-prevBytesRead, 196) && res.Cost == bytesRead-prevBytesRead { + t.Fatalf("expected bytes read for faceted query is around 196, got %v", + bytesRead-prevBytesRead) + } + prevBytesRead = bytesRead + + min := float64(8660) + max := float64(8665) + numRangeQuery := NewNumericRangeQuery(&min, &max) + numRangeQuery.FieldVal = "id" + searchRequest = NewSearchRequestOptions(numRangeQuery, int(10), 0, true) + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 924 && res.Cost == bytesRead-prevBytesRead { + t.Fatalf("expected bytes read for numeric range query is 924, got %v", + bytesRead-prevBytesRead) + } + prevBytesRead = bytesRead + + searchRequest = NewSearchRequestOptions(query, int(10), 0, true) + searchRequest.Highlight = &HighlightRequest{} + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 105 && res.Cost == bytesRead-prevBytesRead { + t.Fatalf("expected bytes read for query with highlighter is 105, got %v", + bytesRead-prevBytesRead) + } + prevBytesRead = bytesRead + + disQuery := NewDisjunctionQuery(NewMatchQuery("hotel"), NewMatchQuery("united")) + searchRequest = NewSearchRequestOptions(disQuery, int(10), 0, true) + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + // expectation is that the bytes read is roughly equal to sum of sub queries in + // the disjunction query plus the segment loading portion for the second subquery + // since it's created afresh and not reused + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 120 && res.Cost == bytesRead-prevBytesRead { + t.Fatalf("expected bytes read for disjunction query is 120, got %v", + bytesRead-prevBytesRead) + } +} + +func TestBytesReadStored(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("hotel", documentMapping) + + indexMapping.DocValuesDynamic = false + indexMapping.StoreDynamic = false + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Store = true + contentFieldMapping.IncludeInAll = false + contentFieldMapping.IncludeTermVectors = false + + reviewsMapping := NewDocumentMapping() + reviewsMapping.AddFieldMappingsAt("content", contentFieldMapping) + documentMapping.AddSubDocumentMapping("reviews", reviewsMapping) + + typeFieldMapping := NewTextFieldMapping() + typeFieldMapping.Store = false + typeFieldMapping.IncludeInAll = false + typeFieldMapping.IncludeTermVectors = false + documentMapping.AddFieldMappingsAt("type", typeFieldMapping) + idx, err := NewUsing(tmpIndexPath, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + batch, err := getBatchFromData(idx, "sample-data.json") + if err != nil { + t.Fatalf("failed to form a batch %v\n", err) + } + err = idx.Batch(batch) + if err != nil { + t.Fatalf("failed to index batch %v\n", err) + } + query := NewTermQuery("hotel") + query.FieldVal = "reviews.content" + searchRequest := NewSearchRequestOptions(query, int(10), 0, true) + res, err := idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + + stats, _ := idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ := stats["num_bytes_read_at_query_time"].(uint64) + + expectedBytesRead := uint64(11911) + if supportForVectorSearch { + expectedBytesRead = 12321 + } + + if bytesRead != expectedBytesRead && bytesRead == res.Cost { + t.Fatalf("expected the bytes read stat to be around %v, got %v", expectedBytesRead, bytesRead) + } + prevBytesRead := bytesRead + + searchRequest = NewSearchRequestOptions(query, int(10), 0, true) + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 48 && bytesRead-prevBytesRead == res.Cost { + t.Fatalf("expected the bytes read stat to be around 48, got %v", bytesRead-prevBytesRead) + } + prevBytesRead = bytesRead + + searchRequest = NewSearchRequestOptions(query, int(10), 0, true) + searchRequest.Fields = []string{"*"} + res, err = idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + + stats, _ = idx.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + + if bytesRead-prevBytesRead != 26511 && bytesRead-prevBytesRead == res.Cost { + t.Fatalf("expected the bytes read stat to be around 26511, got %v", + bytesRead-prevBytesRead) + } + idx.Close() + cleanupTmpIndexPath(t, tmpIndexPath) + + // same type of querying but on field "type" + contentFieldMapping.Store = false + typeFieldMapping.Store = true + + tmpIndexPath1 := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath1) + + idx1, err := NewUsing(tmpIndexPath1, indexMapping, Config.DefaultIndexType, Config.DefaultMemKVStore, nil) + if err != nil { + t.Fatal(err) + } + defer func() { + err := idx1.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch, err = getBatchFromData(idx1, "sample-data.json") + if err != nil { + t.Fatalf("failed to form a batch %v\n", err) + } + err = idx1.Batch(batch) + if err != nil { + t.Fatalf("failed to index batch %v\n", err) + } + + query = NewTermQuery("hotel") + query.FieldVal = "type" + searchRequest = NewSearchRequestOptions(query, int(10), 0, true) + res, err = idx1.Search(searchRequest) + if err != nil { + t.Error(err) + } + + stats, _ = idx1.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + + expectedBytesRead = uint64(4097) + if supportForVectorSearch { + expectedBytesRead = 4507 + } + + if bytesRead != expectedBytesRead && bytesRead == res.Cost { + t.Fatalf("expected the bytes read stat to be around %v, got %v", expectedBytesRead, bytesRead) + } + prevBytesRead = bytesRead + + res, err = idx1.Search(searchRequest) + if err != nil { + t.Error(err) + } + stats, _ = idx1.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 47 && bytesRead-prevBytesRead == res.Cost { + t.Fatalf("expected the bytes read stat to be around 47, got %v", bytesRead-prevBytesRead) + } + prevBytesRead = bytesRead + + searchRequest.Fields = []string{"*"} + res, err = idx1.Search(searchRequest) + if err != nil { + t.Error(err) + } + + stats, _ = idx1.StatsMap()["index"].(map[string]interface{}) + bytesRead, _ = stats["num_bytes_read_at_query_time"].(uint64) + if bytesRead-prevBytesRead != 77 && bytesRead-prevBytesRead == res.Cost { + t.Fatalf("expected the bytes read stat to be around 77, got %v", bytesRead-prevBytesRead) + } +} + +func readDataFromFile(fileName string) ([]map[string]interface{}, error) { + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + path := filepath.Join(pwd, "data", "test", fileName) + + var dataset []map[string]interface{} + fileContent, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + err = json.Unmarshal(fileContent, &dataset) + if err != nil { + return nil, err + } + + return dataset, nil +} + +func getBatchFromData(idx Index, fileName string) (*Batch, error) { + dataset, err := readDataFromFile(fileName) + batch := idx.NewBatch() + for _, doc := range dataset { + err = batch.Index(fmt.Sprintf("%d", doc["id"]), doc) + if err != nil { + return nil, err + } + } + + return batch, err +} + +func TestIndexCreateNewOverExisting(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + err = index.Close() + if err != nil { + t.Fatal(err) + } + + _, err = New(tmpIndexPath, NewIndexMapping()) + if err != ErrorIndexPathExists { + t.Fatalf("expected error index path exists, got %v", err) + } +} + +func TestIndexOpenNonExisting(t *testing.T) { + _, err := Open("doesnotexist") + if err != ErrorIndexPathDoesNotExist { + t.Fatalf("expected error index path does not exist, got %v", err) + } +} + +func TestIndexOpenMetaMissingOrCorrupt(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + err = index.Close() + if err != nil { + t.Fatal(err) + } + + tmpIndexPathMeta := filepath.Join(tmpIndexPath, "index_meta.json") + + // now intentionally change the storage type + err = os.WriteFile(tmpIndexPathMeta, []byte(`{"storage":"mystery"}`), 0o666) + if err != nil { + t.Fatal(err) + } + + _, err = Open(tmpIndexPath) + if err == nil { + t.Fatalf("expected error for unknown storage type, got %v", err) + } + + // now intentionally corrupt the metadata + err = os.WriteFile(tmpIndexPathMeta, []byte("corrupted"), 0o666) + if err != nil { + t.Fatal(err) + } + + _, err = Open(tmpIndexPath) + if err != ErrorIndexMetaCorrupt { + t.Fatalf("expected error index metadata corrupted, got %v", err) + } + + // now intentionally remove the metadata + err = os.Remove(tmpIndexPathMeta) + if err != nil { + t.Fatal(err) + } + + _, err = Open(tmpIndexPath) + if err != ErrorIndexMetaMissing { + t.Fatalf("expected error index metadata missing, got %v", err) + } +} + +func TestInMemIndex(t *testing.T) { + index, err := NewMemOnly(NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestClosedIndex(t *testing.T) { + index, err := NewMemOnly(NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + err = index.Close() + if err != nil { + t.Fatal(err) + } + + err = index.Index("test", "test") + if err != ErrorIndexClosed { + t.Errorf("expected error index closed, got %v", err) + } + + err = index.Delete("test") + if err != ErrorIndexClosed { + t.Errorf("expected error index closed, got %v", err) + } + + b := index.NewBatch() + err = index.Batch(b) + if err != ErrorIndexClosed { + t.Errorf("expected error index closed, got %v", err) + } + + _, err = index.Document("test") + if err != ErrorIndexClosed { + t.Errorf("expected error index closed, got %v", err) + } + + _, err = index.DocCount() + if err != ErrorIndexClosed { + t.Errorf("expected error index closed, got %v", err) + } + + _, err = index.Search(NewSearchRequest(NewTermQuery("test"))) + if err != ErrorIndexClosed { + t.Errorf("expected error index closed, got %v", err) + } + + _, err = index.Fields() + if err != ErrorIndexClosed { + t.Errorf("expected error index closed, got %v", err) + } +} + +type slowQuery struct { + actual query.Query + delay time.Duration +} + +func (s *slowQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + time.Sleep(s.delay) + return s.actual.Searcher(ctx, i, m, options) +} + +func TestSlowSearch(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + defer func() { + // reset logger back to normal + SetLog(log.New(io.Discard, "bleve", log.LstdFlags)) + }() + // set custom logger + var sdw sawDataWriter + SetLog(log.New(&sdw, "bleve", log.LstdFlags)) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + Config.SlowSearchLogThreshold = 1 * time.Minute + + query := NewTermQuery("water") + req := NewSearchRequest(query) + _, err = index.Search(req) + if err != nil { + t.Fatal(err) + } + + if sdw.sawData { + t.Errorf("expected to not see slow query logged, but did") + } + + sq := &slowQuery{ + actual: query, + delay: 50 * time.Millisecond, // on Windows timer resolution is 15ms + } + req.Query = sq + Config.SlowSearchLogThreshold = 1 * time.Microsecond + _, err = index.Search(req) + if err != nil { + t.Fatal(err) + } + + if !sdw.sawData { + t.Errorf("expected to see slow query logged, but didn't") + } +} + +type sawDataWriter struct { + sawData bool +} + +func (s *sawDataWriter) Write(p []byte) (n int, err error) { + s.sawData = true + return len(p), nil +} + +func TestStoredFieldPreserved(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + doca := map[string]interface{}{ + "name": "Marty", + "desc": "GopherCON India", + "bool": true, + "num": float64(1), + } + err = index.Index("a", doca) + if err != nil { + t.Error(err) + } + + q := NewTermQuery("marty") + req := NewSearchRequest(q) + req.Fields = []string{"name", "desc", "bool", "num"} + res, err := index.Search(req) + if err != nil { + t.Error(err) + } + + if len(res.Hits) != 1 { + t.Fatalf("expected 1 hit, got %d", len(res.Hits)) + } + if res.Hits[0].Fields["name"] != "Marty" { + t.Errorf("expected 'Marty' got '%s'", res.Hits[0].Fields["name"]) + } + if res.Hits[0].Fields["desc"] != "GopherCON India" { + t.Errorf("expected 'GopherCON India' got '%s'", res.Hits[0].Fields["desc"]) + } + if res.Hits[0].Fields["num"] != float64(1) { + t.Errorf("expected '1' got '%v'", res.Hits[0].Fields["num"]) + } + if res.Hits[0].Fields["bool"] != true { + t.Errorf("expected 'true' got '%v'", res.Hits[0].Fields["bool"]) + } +} + +func TestDict(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + doca := map[string]interface{}{ + "name": "marty", + "desc": "gophercon india", + } + err = index.Index("a", doca) + if err != nil { + t.Error(err) + } + + docy := map[string]interface{}{ + "name": "jasper", + "desc": "clojure", + } + err = index.Index("y", docy) + if err != nil { + t.Error(err) + } + + docx := map[string]interface{}{ + "name": "rose", + "desc": "googler", + } + err = index.Index("x", docx) + if err != nil { + t.Error(err) + } + + dict, err := index.FieldDict("name") + if err != nil { + t.Error(err) + } + + terms := []string{} + de, err := dict.Next() + for err == nil && de != nil { + terms = append(terms, string(de.Term)) + de, err = dict.Next() + } + + expectedTerms := []string{"jasper", "marty", "rose"} + if !reflect.DeepEqual(terms, expectedTerms) { + t.Errorf("expected %v, got %v", expectedTerms, terms) + } + + err = dict.Close() + if err != nil { + t.Fatal(err) + } + + // test start and end range + dict, err = index.FieldDictRange("name", []byte("marty"), []byte("rose")) + if err != nil { + t.Error(err) + } + + terms = []string{} + de, err = dict.Next() + for err == nil && de != nil { + terms = append(terms, string(de.Term)) + de, err = dict.Next() + } + + expectedTerms = []string{"marty", "rose"} + if !reflect.DeepEqual(terms, expectedTerms) { + t.Errorf("expected %v, got %v", expectedTerms, terms) + } + + err = dict.Close() + if err != nil { + t.Fatal(err) + } + + docz := map[string]interface{}{ + "name": "prefix", + "desc": "bob cat cats catting dog doggy zoo", + } + err = index.Index("z", docz) + if err != nil { + t.Error(err) + } + + dict, err = index.FieldDictPrefix("desc", []byte("cat")) + if err != nil { + t.Error(err) + } + + terms = []string{} + de, err = dict.Next() + for err == nil && de != nil { + terms = append(terms, string(de.Term)) + de, err = dict.Next() + } + + expectedTerms = []string{"cat", "cats", "catting"} + if !reflect.DeepEqual(terms, expectedTerms) { + t.Errorf("expected %v, got %v", expectedTerms, terms) + } + + stats := index.Stats() + if stats == nil { + t.Errorf("expected IndexStat, got nil") + } + + err = dict.Close() + if err != nil { + t.Fatal(err) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestBatchString(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := index.NewBatch() + err = batch.Index("a", []byte("{}")) + if err != nil { + t.Fatal(err) + } + batch.Delete("b") + batch.SetInternal([]byte("c"), []byte{}) + batch.DeleteInternal([]byte("d")) + + batchStr := batch.String() + if !strings.HasPrefix(batchStr, "Batch (2 ops, 2 internal ops)") { + t.Errorf("expected to start with Batch (2 ops, 2 internal ops), did not") + } + if !strings.Contains(batchStr, "INDEX - 'a'") { + t.Errorf("expected to contain INDEX - 'a', did not") + } + if !strings.Contains(batchStr, "DELETE - 'b'") { + t.Errorf("expected to contain DELETE - 'b', did not") + } + if !strings.Contains(batchStr, "SET INTERNAL - 'c'") { + t.Errorf("expected to contain SET INTERNAL - 'c', did not") + } + if !strings.Contains(batchStr, "DELETE INTERNAL - 'd'") { + t.Errorf("expected to contain DELETE INTERNAL - 'd', did not") + } +} + +func TestIndexMetadataRaceBug198(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + wg := sync.WaitGroup{} + wg.Add(1) + done := make(chan struct{}) + go func() { + for { + select { + case <-done: + wg.Done() + return + default: + _, err2 := index.DocCount() + if err2 != nil { + t.Error(err2) + wg.Done() + return + } + } + } + }() + + for i := 0; i < 100; i++ { + batch := index.NewBatch() + err = batch.Index("a", []byte("{}")) + if err != nil { + t.Fatal(err) + } + err = index.Batch(batch) + if err != nil { + t.Fatal(err) + } + } + + close(done) + wg.Wait() +} + +func TestSortMatchSearch(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + names := []string{"Noam", "Uri", "David", "Yosef", "Eitan", "Itay", "Ariel", "Daniel", "Omer", "Yogev", "Yehonatan", "Moshe", "Mohammed", "Yusuf", "Omar"} + days := []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} + numbers := []string{"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"} + b := index.NewBatch() + for i := 0; i < 200; i++ { + doc := make(map[string]interface{}) + doc["Name"] = names[i%len(names)] + doc["Day"] = days[i%len(days)] + doc["Number"] = numbers[i%len(numbers)] + err = b.Index(fmt.Sprintf("%d", i), doc) + if err != nil { + t.Fatal(err) + } + } + err = index.Batch(b) + if err != nil { + t.Fatal(err) + } + + req := NewSearchRequest(NewMatchQuery("One")) + req.SortBy([]string{"Day", "Name"}) + req.Fields = []string{"*"} + sr, err := index.Search(req) + if err != nil { + t.Fatal(err) + } + prev := "" + for _, hit := range sr.Hits { + val := hit.Fields["Day"].(string) + if prev > val { + t.Errorf("Hits must be sorted by 'Day'. Found '%s' before '%s'", prev, val) + } + prev = val + } + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexCountMatchSearch(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(i int) { + b := index.NewBatch() + for j := 0; j < 200; j++ { + id := fmt.Sprintf("%d", (i*200)+j) + + doc := struct { + Body string + }{ + Body: "match", + } + + err := b.Index(id, doc) + if err != nil { + t.Error(err) + wg.Done() + return + } + } + + err := index.Batch(b) + if err != nil { + t.Error(err) + wg.Done() + return + } + + wg.Done() + }(i) + } + wg.Wait() + + // search for something that should match all documents + sr, err := index.Search(NewSearchRequest(NewMatchQuery("match"))) + if err != nil { + t.Fatal(err) + } + + // get the index document count + dc, err := index.DocCount() + if err != nil { + t.Fatal(err) + } + + // make sure test is working correctly, doc count should 2000 + if dc != 2000 { + t.Errorf("expected doc count 2000, got %d", dc) + } + + // make sure our search found all the documents + if dc != sr.Total { + t.Errorf("expected search result total %d to match doc count %d", sr.Total, dc) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestBatchReset(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + batch := index.NewBatch() + err = batch.Index("k1", struct { + Body string + }{ + Body: "v1", + }) + if err != nil { + t.Error(err) + } + batch.Delete("k2") + batch.SetInternal([]byte("k3"), []byte("v3")) + batch.DeleteInternal([]byte("k4")) + + if batch.Size() != 4 { + t.Logf("%v", batch) + t.Errorf("expected batch size 4, got %d", batch.Size()) + } + + batch.Reset() + + if batch.Size() != 0 { + t.Errorf("expected batch size 0 after reset, got %d", batch.Size()) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestDocumentFieldArrayPositions(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + // index a document with an array of strings + err = idx.Index("k", struct { + Messages []string + }{ + Messages: []string{ + "first", + "second", + "third", + "last", + }, + }) + if err != nil { + t.Fatal(err) + } + + // load the document + doc, err := idx.Document("k") + if err != nil { + t.Fatal(err) + } + + doc.VisitFields(func(f index.Field) { + if reflect.DeepEqual(f.Value(), []byte("first")) { + ap := f.ArrayPositions() + if len(ap) < 1 { + t.Errorf("expected an array position, got none") + return + } + if ap[0] != 0 { + t.Errorf("expected 'first' in array position 0, got %d", ap[0]) + } + } + if reflect.DeepEqual(f.Value(), []byte("second")) { + ap := f.ArrayPositions() + if len(ap) < 1 { + t.Errorf("expected an array position, got none") + return + } + if ap[0] != 1 { + t.Errorf("expected 'second' in array position 1, got %d", ap[0]) + } + } + if reflect.DeepEqual(f.Value(), []byte("third")) { + ap := f.ArrayPositions() + if len(ap) < 1 { + t.Errorf("expected an array position, got none") + return + } + if ap[0] != 2 { + t.Errorf("expected 'third' in array position 2, got %d", ap[0]) + } + } + if reflect.DeepEqual(f.Value(), []byte("last")) { + ap := f.ArrayPositions() + if len(ap) < 1 { + t.Errorf("expected an array position, got none") + return + } + if ap[0] != 3 { + t.Errorf("expected 'last' in array position 3, got %d", ap[0]) + } + } + }) + + // now index a document in the same field with a single string + err = idx.Index("k2", struct { + Messages string + }{ + Messages: "only", + }) + if err != nil { + t.Fatal(err) + } + + // load the document + doc, err = idx.Document("k2") + if err != nil { + t.Fatal(err) + } + + doc.VisitFields(func(f index.Field) { + if reflect.DeepEqual(f.Value(), []byte("only")) { + ap := f.ArrayPositions() + if len(ap) != 0 { + t.Errorf("expected no array positions, got %d", len(ap)) + return + } + } + }) + + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestKeywordSearchBug207(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + f := NewTextFieldMapping() + f.Analyzer = keyword.Name + + m := NewIndexMapping() + m.DefaultMapping = NewDocumentMapping() + m.DefaultMapping.AddFieldMappingsAt("Body", f) + + index, err := New(tmpIndexPath, m) + if err != nil { + t.Fatal(err) + } + + doc1 := struct { + Body string + }{ + Body: "a555c3bb06f7a127cda000005", + } + + err = index.Index("a", doc1) + if err != nil { + t.Fatal(err) + } + + doc2 := struct { + Body string + }{ + Body: "555c3bb06f7a127cda000005", + } + + err = index.Index("b", doc2) + if err != nil { + t.Fatal(err) + } + + // now search for these terms + sreq := NewSearchRequest(NewTermQuery("a555c3bb06f7a127cda000005")) + sres, err := index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 result, got %d", sres.Total) + } + if sres.Hits[0].ID != "a" { + t.Errorf("expecated id 'a', got '%s'", sres.Hits[0].ID) + } + + sreq = NewSearchRequest(NewTermQuery("555c3bb06f7a127cda000005")) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 result, got %d", sres.Total) + } + if sres.Hits[0].ID != "b" { + t.Errorf("expecated id 'b', got '%s'", sres.Hits[0].ID) + } + + // now do the same searches using query strings + sreq = NewSearchRequest(NewQueryStringQuery("Body:a555c3bb06f7a127cda000005")) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 result, got %d", sres.Total) + } + if sres.Hits[0].ID != "a" { + t.Errorf("expecated id 'a', got '%s'", sres.Hits[0].ID) + } + + sreq = NewSearchRequest(NewQueryStringQuery(`Body:555c3bb06f7a127cda000005`)) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 result, got %d", sres.Total) + } + if sres.Hits[0].ID != "b" { + t.Errorf("expecated id 'b', got '%s'", sres.Hits[0].ID) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestTermVectorArrayPositions(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + // index a document with an array of strings + err = index.Index("k", struct { + Messages []string + }{ + Messages: []string{ + "first", + "second", + "third", + "last", + }, + }) + if err != nil { + t.Fatal(err) + } + + // search for this document in all field + tq := NewTermQuery("second") + tsr := NewSearchRequest(tq) + tsr.IncludeLocations = true + results, err := index.Search(tsr) + if err != nil { + t.Fatal(err) + } + if results.Total != 1 { + t.Fatalf("expected 1 result, got %d", results.Total) + } + if len(results.Hits[0].Locations["Messages"]["second"]) < 1 { + t.Fatalf("expected at least one location") + } + if len(results.Hits[0].Locations["Messages"]["second"][0].ArrayPositions) < 1 { + t.Fatalf("expected at least one location array position") + } + if results.Hits[0].Locations["Messages"]["second"][0].ArrayPositions[0] != 1 { + t.Fatalf("expected array position 1, got %d", results.Hits[0].Locations["Messages"]["second"][0].ArrayPositions[0]) + } + + // repeat search for this document in Messages field + tq2 := NewTermQuery("third") + tq2.SetField("Messages") + tsr = NewSearchRequest(tq2) + tsr.IncludeLocations = true + results, err = index.Search(tsr) + if err != nil { + t.Fatal(err) + } + if results.Total != 1 { + t.Fatalf("expected 1 result, got %d", results.Total) + } + if len(results.Hits[0].Locations["Messages"]["third"]) < 1 { + t.Fatalf("expected at least one location") + } + if len(results.Hits[0].Locations["Messages"]["third"][0].ArrayPositions) < 1 { + t.Fatalf("expected at least one location array position") + } + if results.Hits[0].Locations["Messages"]["third"][0].ArrayPositions[0] != 2 { + t.Fatalf("expected array position 2, got %d", results.Hits[0].Locations["Messages"]["third"][0].ArrayPositions[0]) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestDocumentStaticMapping(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + m := NewIndexMapping() + m.DefaultMapping = NewDocumentStaticMapping() + m.DefaultMapping.AddFieldMappingsAt("Text", NewTextFieldMapping()) + m.DefaultMapping.AddFieldMappingsAt("Date", NewDateTimeFieldMapping()) + m.DefaultMapping.AddFieldMappingsAt("Numeric", NewNumericFieldMapping()) + + index, err := New(tmpIndexPath, m) + if err != nil { + t.Fatal(err) + } + + doc1 := struct { + Text string + IgnoredText string + Numeric float64 + IgnoredNumeric float64 + Date time.Time + IgnoredDate time.Time + }{ + Text: "valid text", + IgnoredText: "ignored text", + Numeric: 10, + IgnoredNumeric: 20, + Date: time.Unix(1, 0), + IgnoredDate: time.Unix(2, 0), + } + + err = index.Index("a", doc1) + if err != nil { + t.Fatal(err) + } + + fields, err := index.Fields() + if err != nil { + t.Fatal(err) + } + sort.Strings(fields) + expectedFields := []string{"Date", "Numeric", "Text", "_all"} + if len(fields) < len(expectedFields) { + t.Fatalf("invalid field count: %d", len(fields)) + } + for i, expected := range expectedFields { + if expected != fields[i] { + t.Fatalf("unexpected field[%d]: %s", i, fields[i]) + } + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexEmptyDocId(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := map[string]interface{}{ + "body": "nodocid", + } + + err = index.Index("", doc) + if err != ErrorEmptyID { + t.Errorf("expect index empty doc id to fail") + } + + err = index.Delete("") + if err != ErrorEmptyID { + t.Errorf("expect delete empty doc id to fail") + } + + batch := index.NewBatch() + err = batch.Index("", doc) + if err != ErrorEmptyID { + t.Errorf("expect index empty doc id in batch to fail") + } + + batch.Delete("") + if batch.Size() > 0 { + t.Errorf("expect delete empty doc id in batch to be ignored") + } +} + +func TestDateTimeFieldMappingIssue287(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + f := NewDateTimeFieldMapping() + + m := NewIndexMapping() + m.DefaultMapping = NewDocumentMapping() + m.DefaultMapping.AddFieldMappingsAt("Date", f) + + index, err := New(tmpIndexPath, m) + if err != nil { + t.Fatal(err) + } + + type doc struct { + Date time.Time + } + + now := time.Now() + + // 3hr ago to 1hr ago + for i := 0; i < 3; i++ { + d := doc{now.Add(time.Duration((i - 3)) * time.Hour)} + + err = index.Index(strconv.FormatInt(int64(i), 10), d) + if err != nil { + t.Fatal(err) + } + } + + // search range across all docs + start := now.Add(-4 * time.Hour) + end := now + sreq := NewSearchRequest(NewDateRangeQuery(start, end)) + sres, err := index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 3 { + t.Errorf("expected 3 results, got %d", sres.Total) + } + + // search range includes only oldest + start = now.Add(-4 * time.Hour) + end = now.Add(-121 * time.Minute) + sreq = NewSearchRequest(NewDateRangeQuery(start, end)) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + if sres.Total > 0 && sres.Hits[0].ID != "0" { + t.Errorf("expecated id '0', got '%s'", sres.Hits[0].ID) + } + + // search range includes only newest + start = now.Add(-61 * time.Minute) + end = now + sreq = NewSearchRequest(NewDateRangeQuery(start, end)) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + if sres.Total > 0 && sres.Hits[0].ID != "2" { + t.Errorf("expecated id '2', got '%s'", sres.Hits[0].ID) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestDocumentFieldArrayPositionsBug295(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + // index a document with an array of strings + err = index.Index("k", struct { + Messages []string + Another string + MoreData []string + }{ + Messages: []string{ + "bleve", + "bleve", + }, + Another: "text", + MoreData: []string{ + "a", + "b", + "c", + "bleve", + }, + }) + if err != nil { + t.Fatal(err) + } + + // search for it in the messages field + tq := NewTermQuery("bleve") + tq.SetField("Messages") + tsr := NewSearchRequest(tq) + tsr.IncludeLocations = true + results, err := index.Search(tsr) + if err != nil { + t.Fatal(err) + } + if results.Total != 1 { + t.Fatalf("expected 1 result, got %d", results.Total) + } + if len(results.Hits[0].Locations["Messages"]["bleve"]) != 2 { + t.Fatalf("expected 2 locations of 'bleve', got %d", len(results.Hits[0].Locations["Messages"]["bleve"])) + } + if results.Hits[0].Locations["Messages"]["bleve"][0].ArrayPositions[0] != 0 { + t.Errorf("expected array position to be 0") + } + if results.Hits[0].Locations["Messages"]["bleve"][1].ArrayPositions[0] != 1 { + t.Errorf("expected array position to be 1") + } + + // search for it in all + tq = NewTermQuery("bleve") + tsr = NewSearchRequest(tq) + tsr.IncludeLocations = true + results, err = index.Search(tsr) + if err != nil { + t.Fatal(err) + } + if results.Total != 1 { + t.Fatalf("expected 1 result, got %d", results.Total) + } + if len(results.Hits[0].Locations["Messages"]["bleve"]) != 2 { + t.Fatalf("expected 2 locations of 'bleve', got %d", len(results.Hits[0].Locations["Messages"]["bleve"])) + } + if results.Hits[0].Locations["Messages"]["bleve"][0].ArrayPositions[0] != 0 { + t.Errorf("expected array position to be 0") + } + if results.Hits[0].Locations["Messages"]["bleve"][1].ArrayPositions[0] != 1 { + t.Errorf("expected array position to be 1") + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestBooleanFieldMappingIssue109(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + m := NewIndexMapping() + m.DefaultMapping = NewDocumentMapping() + m.DefaultMapping.AddFieldMappingsAt("Bool", NewBooleanFieldMapping()) + + index, err := New(tmpIndexPath, m) + if err != nil { + t.Fatal(err) + } + + type doc struct { + Bool bool + } + err = index.Index("true", &doc{Bool: true}) + if err != nil { + t.Fatal(err) + } + err = index.Index("false", &doc{Bool: false}) + if err != nil { + t.Fatal(err) + } + + q := NewBoolFieldQuery(true) + q.SetField("Bool") + sreq := NewSearchRequest(q) + sres, err := index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + + q = NewBoolFieldQuery(false) + q.SetField("Bool") + sreq = NewSearchRequest(q) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + + sreq = NewSearchRequest(NewBoolFieldQuery(true)) + sres, err = index.Search(sreq) + if err != nil { + t.Fatal(err) + } + if sres.Total != 1 { + t.Errorf("expected 1 results, got %d", sres.Total) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestSearchTimeout(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + // first run a search with an absurdly long timeout (should succeed) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + query := NewTermQuery("water") + req := NewSearchRequest(query) + _, err = index.SearchInContext(ctx, req) + if err != nil { + t.Fatal(err) + } + + // now run a search again with an absurdly low timeout (should timeout) + ctx, cancel = context.WithTimeout(context.Background(), 1*time.Microsecond) + defer cancel() + sq := &slowQuery{ + actual: query, + delay: 50 * time.Millisecond, // on Windows timer resolution is 15ms + } + req.Query = sq + _, err = index.SearchInContext(ctx, req) + if err != context.DeadlineExceeded { + t.Fatalf("exected %v, got: %v", context.DeadlineExceeded, err) + } + + // now run a search with a long timeout, but with a long query, and cancel it + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) + sq = &slowQuery{ + actual: query, + delay: 100 * time.Millisecond, // on Windows timer resolution is 15ms + } + req = NewSearchRequest(sq) + cancel() + _, err = index.SearchInContext(ctx, req) + if err != context.Canceled { + t.Fatalf("exected %v, got: %v", context.Canceled, err) + } +} + +// TestConfigCache exposes a concurrent map write with go 1.6 +func TestConfigCache(t *testing.T) { + for i := 0; i < 100; i++ { + go func() { + _, err := Config.Cache.HighlighterNamed(Config.DefaultHighlighter) + if err != nil { + t.Error(err) + } + }() + } +} + +func TestBatchRaceBug260(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + i, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := i.Close() + if err != nil { + t.Fatal(err) + } + }() + b := i.NewBatch() + err = b.Index("1", 1) + if err != nil { + t.Fatal(err) + } + err = i.Batch(b) + if err != nil { + t.Fatal(err) + } + b.Reset() + err = b.Index("2", 2) + if err != nil { + t.Fatal(err) + } + err = i.Batch(b) + if err != nil { + t.Fatal(err) + } + b.Reset() +} + +func BenchmarkBatchOverhead(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + m := NewIndexMapping() + i, err := NewUsing(tmpIndexPath, m, Config.DefaultIndexType, null.Name, nil) + if err != nil { + b.Fatal(err) + } + for n := 0; n < b.N; n++ { + // put 1000 items in a batch + batch := i.NewBatch() + for i := 0; i < 1000; i++ { + err = batch.Index(fmt.Sprintf("%d", i), map[string]interface{}{"name": "bleve"}) + if err != nil { + b.Fatal(err) + } + } + err = i.Batch(batch) + if err != nil { + b.Fatal(err) + } + batch.Reset() + } +} + +func TestOpenReadonlyMultiple(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + // build an index and close it + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + doca := map[string]interface{}{ + "name": "marty", + "desc": "gophercon india", + } + err = index.Index("a", doca) + if err != nil { + t.Fatal(err) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } + + // now open it read-only + index, err = OpenUsing(tmpIndexPath, map[string]interface{}{ + "read_only": true, + }) + if err != nil { + t.Fatal(err) + } + + // now open it again + index2, err := OpenUsing(tmpIndexPath, map[string]interface{}{ + "read_only": true, + }) + if err != nil { + t.Fatal(err) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } + err = index2.Close() + if err != nil { + t.Fatal(err) + } +} + +// TestBug408 tests for VERY large values of size, even though actual result +// set may be reasonable size +func TestBug408(t *testing.T) { + type TestStruct struct { + ID string `json:"id"` + UserID *string `json:"user_id"` + } + + docMapping := NewDocumentMapping() + docMapping.AddFieldMappingsAt("id", NewTextFieldMapping()) + docMapping.AddFieldMappingsAt("user_id", NewTextFieldMapping()) + + indexMapping := NewIndexMapping() + indexMapping.DefaultMapping = docMapping + + index, err := NewMemOnly(indexMapping) + if err != nil { + t.Fatal(err) + } + + numToTest := 10 + matchUserID := "match" + noMatchUserID := "no_match" + matchingDocIds := make(map[string]struct{}) + + for i := 0; i < numToTest; i++ { + ds := &TestStruct{"id_" + strconv.Itoa(i), nil} + if i%2 == 0 { + ds.UserID = &noMatchUserID + } else { + ds.UserID = &matchUserID + matchingDocIds[ds.ID] = struct{}{} + } + err = index.Index(ds.ID, ds) + if err != nil { + t.Fatal(err) + } + } + + cnt, err := index.DocCount() + if err != nil { + t.Fatal(err) + } + if int(cnt) != numToTest { + t.Fatalf("expected %d documents in index, got %d", numToTest, cnt) + } + + q := NewTermQuery(matchUserID) + q.SetField("user_id") + searchReq := NewSearchRequestOptions(q, math.MaxInt32, 0, false) + results, err := index.Search(searchReq) + if err != nil { + t.Fatal(err) + } + if int(results.Total) != numToTest/2 { + t.Fatalf("expected %d search hits, got %d", numToTest/2, results.Total) + } + + for _, result := range results.Hits { + if _, found := matchingDocIds[result.ID]; !found { + t.Fatalf("document with ID %s not in results as expected", result.ID) + } + } +} + +func TestIndexAdvancedCountMatchSearch(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + var wg sync.WaitGroup + errChan := make(chan error, 10) + for i := 0; i < 10; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + b := index.NewBatch() + for j := 0; j < 200; j++ { + id := fmt.Sprintf("%d", (i*200)+j) + + doc := document.NewDocument(id) + doc.AddField(document.NewTextField("body", []uint64{}, []byte("match"))) + doc.AddField(document.NewCompositeField("_all", true, []string{}, []string{})) + + err := b.IndexAdvanced(doc) + if err != nil { + errChan <- err + return + } + } + err := index.Batch(b) + if err != nil { + errChan <- err + return + } + }(i) + } + wg.Wait() + + close(errChan) + for err := range errChan { + if err != nil { + t.Fatal(err) + } + } + + // search for something that should match all documents + sr, err := index.Search(NewSearchRequest(NewMatchQuery("match"))) + if err != nil { + t.Fatal(err) + } + + // get the index document count + dc, err := index.DocCount() + if err != nil { + t.Fatal(err) + } + + // make sure test is working correctly, doc count should 2000 + if dc != 2000 { + t.Errorf("expected doc count 2000, got %d", dc) + } + + // make sure our search found all the documents + if dc != sr.Total { + t.Errorf("expected search result total %d to match doc count %d", sr.Total, dc) + } + + err = index.Close() + if err != nil { + t.Fatal(err) + } +} + +func benchmarkSearchOverhead(indexType string, b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + index, err := NewUsing(tmpIndexPath, NewIndexMapping(), + indexType, Config.DefaultKVStore, nil) + if err != nil { + b.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + b.Fatal(err) + } + }() + + elements := []string{"air", "water", "fire", "earth"} + for j := 0; j < 10000; j++ { + err = index.Index(fmt.Sprintf("%d", j), + map[string]interface{}{"name": elements[j%len(elements)]}) + if err != nil { + b.Fatal(err) + } + } + + query1 := NewTermQuery("water") + query2 := NewTermQuery("fire") + query := NewDisjunctionQuery(query1, query2) + req := NewSearchRequest(query) + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _, err = index.Search(req) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkUpsidedownSearchOverhead(b *testing.B) { + benchmarkSearchOverhead(upsidedown.Name, b) +} + +func BenchmarkScorchSearchOverhead(b *testing.B) { + benchmarkSearchOverhead(scorch.Name, b) +} + +func TestSearchQueryCallback(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + index, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + query := NewTermQuery("water") + req := NewSearchRequest(query) + + expErr := fmt.Errorf("MEM_LIMIT_EXCEEDED") + f := func(size uint64) error { + // the intended usage of this callback is to see the estimated + // memory usage before executing, and possibly abort early + // in this test we simulate returning such an error + return expErr + } + + ctx := context.WithValue(context.Background(), SearchQueryStartCallbackKey, SearchQueryStartCallbackFn(f)) + _, err = index.SearchInContext(ctx, req) + if err != expErr { + t.Fatalf("Expected: %v, Got: %v", expErr, err) + } +} + +func TestBatchMerge(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + doca := map[string]interface{}{ + "name": "scorch", + "desc": "gophercon india", + "nation": "india", + } + + batchA := idx.NewBatch() + err = batchA.Index("a", doca) + if err != nil { + t.Error(err) + } + batchA.SetInternal([]byte("batchkA"), []byte("batchvA")) + + docb := map[string]interface{}{ + "name": "moss", + "desc": "gophercon MV", + } + + batchB := idx.NewBatch() + err = batchB.Index("b", docb) + if err != nil { + t.Error(err) + } + batchB.SetInternal([]byte("batchkB"), []byte("batchvB")) + + docC := map[string]interface{}{ + "name": "blahblah", + "desc": "inProgress", + "country": "usa", + } + + batchC := idx.NewBatch() + err = batchC.Index("c", docC) + if err != nil { + t.Error(err) + } + batchC.SetInternal([]byte("batchkC"), []byte("batchvC")) + batchC.SetInternal([]byte("batchkB"), []byte("batchvBNew")) + batchC.Delete("a") + batchC.DeleteInternal([]byte("batchkA")) + + batchA.Merge(batchB) + + if batchA.Size() != 4 { + t.Errorf("expected batch size 4, got %d", batchA.Size()) + } + + batchA.Merge(batchC) + + if batchA.Size() != 6 { + t.Errorf("expected batch size 6, got %d", batchA.Size()) + } + + err = idx.Batch(batchA) + if err != nil { + t.Fatal(err) + } + + // close the index, open it again, and try some more things + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + idx, err = Open(tmpIndexPath) + if err != nil { + t.Fatal(err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + count, err := idx.DocCount() + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Errorf("expected doc count 2, got %d", count) + } + + doc, err := idx.Document("c") + if err != nil { + t.Fatal(err) + } + + val, err := idx.GetInternal([]byte("batchkB")) + if err != nil { + t.Fatal(err) + } + if val == nil || string(val) != "batchvBNew" { + t.Errorf("expected val: batchvBNew , got %s", val) + } + + val, err = idx.GetInternal([]byte("batchkA")) + if err != nil { + t.Fatal(err) + } + if val != nil { + t.Errorf("expected nil, got %s", val) + } + + foundNameField := false + doc.VisitFields(func(field index.Field) { + if field.Name() == "name" && string(field.Value()) == "blahblah" { + foundNameField = true + } + }) + if !foundNameField { + t.Errorf("expected to find field named 'name' with value 'blahblah'") + } + + fields, err := idx.Fields() + if err != nil { + t.Fatal(err) + } + + expectedFields := map[string]bool{ + "_all": false, + "name": false, + "desc": false, + "country": false, + } + if len(fields) < len(expectedFields) { + t.Fatalf("expected %d fields got %d", len(expectedFields), len(fields)) + } + + for _, f := range fields { + expectedFields[f] = true + } + + for ef, efp := range expectedFields { + if !efp { + t.Errorf("field %s is missing", ef) + } + } +} + +func TestBug1096(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + // use default mapping + mapping := NewIndexMapping() + + // create a scorch index with default SAFE batches + var idx Index + idx, err = NewUsing(tmpIndexPath, mapping, "scorch", "scorch", nil) + if err != nil { + log.Fatal(err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // create a single batch instance that we will reuse + // this should be safe because we have single goroutine + // and we always wait for batch execution to finish + batch := idx.NewBatch() + + // number of batches to execute + for i := 0; i < 10; i++ { + + // number of documents to put into the batch + for j := 0; j < 91; j++ { + + // create a doc id 0-90 (important so that we get id's 9 and 90) + // this could duplicate something already in the index + // this too should be OK and update the item in the index + id := fmt.Sprintf("%d", j) + + err = batch.Index(id, map[string]interface{}{ + "name": id, + "batch": fmt.Sprintf("%d", i), + }) + if err != nil { + log.Fatal(err) + } + } + + // execute the batch + err = idx.Batch(batch) + if err != nil { + log.Fatal(err) + } + + // reset the batch before reusing it + batch.Reset() + } + + // search for docs having name starting with the number 9 + q := NewWildcardQuery("9*") + q.SetField("name") + req := NewSearchRequestOptions(q, 1000, 0, false) + req.Fields = []string{"*"} + var res *SearchResult + res, err = idx.Search(req) + if err != nil { + log.Fatal(err) + } + + // we expect only 2 hits, for docs 9 and 90 + if res.Total > 2 { + t.Fatalf("expected only 2 hits '9' and '90', got %v", res) + } +} + +func TestDataRaceBug1092(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + // use default mapping + mapping := NewIndexMapping() + + var idx Index + idx, err = NewUsing(tmpIndexPath, mapping, upsidedown.Name, boltdb.Name, nil) + if err != nil { + log.Fatal(err) + } + defer func() { + cerr := idx.Close() + if cerr != nil { + t.Fatal(cerr) + } + }() + + batch := idx.NewBatch() + for i := 0; i < 10; i++ { + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + + batch.Reset() + } +} + +func TestBatchRaceBug1149(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + i, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := i.Close() + if err != nil { + t.Fatal(err) + } + }() + testBatchRaceBug1149(t, i) +} + +func TestBatchRaceBug1149Scorch(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + i, err := NewUsing(tmpIndexPath, NewIndexMapping(), "scorch", "scorch", nil) + if err != nil { + t.Fatal(err) + } + defer func() { + err := i.Close() + if err != nil { + t.Fatal(err) + } + }() + testBatchRaceBug1149(t, i) +} + +func testBatchRaceBug1149(t *testing.T, i Index) { + b := i.NewBatch() + b.Delete("1") + err = i.Batch(b) + if err != nil { + t.Fatal(err) + } + b.Reset() + err = i.Batch(b) + if err != nil { + t.Fatal(err) + } + b.Reset() +} + +func TestOptimisedConjunctionSearchHits(t *testing.T) { + scorch.OptimizeDisjunctionUnadorned = false + defer func() { + scorch.OptimizeDisjunctionUnadorned = true + }() + + defer func() { + err := os.RemoveAll("testidx") + if err != nil { + t.Fatal(err) + } + }() + + idx, err := NewUsing("testidx", NewIndexMapping(), "scorch", "scorch", nil) + if err != nil { + t.Fatal(err) + } + doca := map[string]interface{}{ + "country": "united", + "name": "Mercure Hotel", + "directions": "B560 and B56 Follow signs to the M56", + } + docb := map[string]interface{}{ + "country": "united", + "name": "Mercure Altrincham Bowdon Hotel", + "directions": "A570 and A57 Follow signs to the M56 Manchester Airport", + } + docc := map[string]interface{}{ + "country": "india united", + "name": "Sonoma Hotel", + "directions": "Northwest", + } + docd := map[string]interface{}{ + "country": "United Kingdom", + "name": "Cresta Court Hotel", + "directions": "junction of A560 and A56", + } + + b := idx.NewBatch() + err = b.Index("a", doca) + if err != nil { + t.Error(err) + } + err = b.Index("b", docb) + if err != nil { + t.Error(err) + } + err = b.Index("c", docc) + if err != nil { + t.Error(err) + } + err = b.Index("d", docd) + if err != nil { + t.Error(err) + } + // execute the batch + err = idx.Batch(b) + if err != nil { + log.Fatal(err) + } + + mq := NewMatchQuery("united") + mq.SetField("country") + + cq := NewConjunctionQuery(mq) + + mq1 := NewMatchQuery("hotel") + mq1.SetField("name") + cq.AddQuery(mq1) + + mq2 := NewMatchQuery("56") + mq2.SetField("directions") + mq2.SetFuzziness(1) + cq.AddQuery(mq2) + + req := NewSearchRequest(cq) + req.Score = "none" + + res, err := idx.Search(req) + if err != nil { + t.Fatal(err) + } + hitsWithOutScore := res.Total + + req = NewSearchRequest(cq) + req.Score = "" + + res, err = idx.Search(req) + if err != nil { + t.Fatal(err) + } + hitsWithScore := res.Total + + if hitsWithOutScore != hitsWithScore { + t.Errorf("expected %d hits without score, got %d", hitsWithScore, hitsWithOutScore) + } + + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestIndexMappingDocValuesDynamic(t *testing.T) { + im := NewIndexMapping() + // DocValuesDynamic's default is true + // Now explicitly set it to false + im.DocValuesDynamic = false + + // Next, retrieve the JSON dump of the index mapping + var data []byte + data, err = json.Marshal(im) + if err != nil { + t.Fatal(err) + } + + // Now, edit an unrelated setting in the index mapping + var m map[string]interface{} + err = json.Unmarshal(data, &m) + if err != nil { + t.Fatal(err) + } + m["index_dynamic"] = false + data, err = json.Marshal(m) + if err != nil { + t.Fatal(err) + } + + // Unmarshal back the changes into the index mapping struct + if err = im.UnmarshalJSON(data); err != nil { + t.Fatal(err) + } + + // Expect DocValuesDynamic to remain false! + if im.DocValuesDynamic { + t.Fatalf("Expected DocValuesDynamic to remain false after the index mapping edit") + } +} + +func TestCopyIndex(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doca := map[string]interface{}{ + "name": "tester", + "desc": "gophercon india testing", + } + err = idx.Index("a", doca) + if err != nil { + t.Error(err) + } + + docy := map[string]interface{}{ + "name": "jasper", + "desc": "clojure", + } + err = idx.Index("y", docy) + if err != nil { + t.Error(err) + } + + err = idx.Delete("y") + if err != nil { + t.Error(err) + } + + docx := map[string]interface{}{ + "name": "rose", + "desc": "xoogler", + } + err = idx.Index("x", docx) + if err != nil { + t.Error(err) + } + + err = idx.SetInternal([]byte("status"), []byte("pending")) + if err != nil { + t.Error(err) + } + + docb := map[string]interface{}{ + "name": "sree", + "desc": "cbft janitor", + } + batch := idx.NewBatch() + err = batch.Index("b", docb) + if err != nil { + t.Error(err) + } + batch.Delete("x") + batch.SetInternal([]byte("batchi"), []byte("batchv")) + batch.DeleteInternal([]byte("status")) + err = idx.Batch(batch) + if err != nil { + t.Error(err) + } + val, err := idx.GetInternal([]byte("batchi")) + if err != nil { + t.Error(err) + } + if string(val) != "batchv" { + t.Errorf("expected 'batchv', got '%s'", val) + } + val, err = idx.GetInternal([]byte("status")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got '%s'", val) + } + + err = idx.SetInternal([]byte("seqno"), []byte("7")) + if err != nil { + t.Error(err) + } + err = idx.SetInternal([]byte("status"), []byte("ready")) + if err != nil { + t.Error(err) + } + err = idx.DeleteInternal([]byte("status")) + if err != nil { + t.Error(err) + } + val, err = idx.GetInternal([]byte("status")) + if err != nil { + t.Error(err) + } + if val != nil { + t.Errorf("expected nil, got '%s'", val) + } + + val, err = idx.GetInternal([]byte("seqno")) + if err != nil { + t.Error(err) + } + if string(val) != "7" { + t.Errorf("expected '7', got '%s'", val) + } + + count, err := idx.DocCount() + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Errorf("expected doc count 2, got %d", count) + } + + doc, err := idx.Document("a") + if err != nil { + t.Fatal(err) + } + foundNameField := false + doc.VisitFields(func(field index.Field) { + if field.Name() == "name" && string(field.Value()) == "tester" { + foundNameField = true + } + }) + if !foundNameField { + t.Errorf("expected to find field named 'name' with value 'tester'") + } + + fields, err := idx.Fields() + if err != nil { + t.Fatal(err) + } + expectedFields := map[string]bool{ + "_all": false, + "name": false, + "desc": false, + } + if len(fields) < len(expectedFields) { + t.Fatalf("expected %d fields got %d", len(expectedFields), len(fields)) + } + for _, f := range fields { + expectedFields[f] = true + } + for ef, efp := range expectedFields { + if !efp { + t.Errorf("field %s is missing", ef) + } + } + + // now create a copy of the index, and repeat assertions on it + copyableIndex, ok := idx.(IndexCopyable) + if !ok { + t.Fatal("index doesn't support copy") + } + backupIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, backupIndexPath) + + err = copyableIndex.CopyTo(FileSystemDirectory(backupIndexPath)) + if err != nil { + t.Fatalf("error copying the index: %v", err) + } + + // open the copied index + idxCopied, err := Open(backupIndexPath) + if err != nil { + t.Fatalf("unable to open copy index") + } + defer func() { + err := idxCopied.Close() + if err != nil { + t.Fatalf("error closing copy index: %v", err) + } + }() + + // assertions on copied index + copyCount, err := idxCopied.DocCount() + if err != nil { + t.Fatal(err) + } + if copyCount != 2 { + t.Errorf("expected doc count 2, got %d", copyCount) + } + + copyDoc, err := idxCopied.Document("a") + if err != nil { + t.Fatal(err) + } + copyFoundNameField := false + copyDoc.VisitFields(func(field index.Field) { + if field.Name() == "name" && string(field.Value()) == "tester" { + copyFoundNameField = true + } + }) + if !copyFoundNameField { + t.Errorf("expected copy index to find field named 'name' with value 'tester'") + } + + copyFields, err := idx.Fields() + if err != nil { + t.Fatal(err) + } + copyExpectedFields := map[string]bool{ + "_all": false, + "name": false, + "desc": false, + } + if len(copyFields) < len(copyExpectedFields) { + t.Fatalf("expected %d fields got %d", len(copyExpectedFields), len(copyFields)) + } + for _, f := range copyFields { + copyExpectedFields[f] = true + } + for ef, efp := range copyExpectedFields { + if !efp { + t.Errorf("copy field %s is missing", ef) + } + } +} + +func TestFuzzyScoring(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + mp := NewIndexMapping() + mp.DefaultAnalyzer = "simple" + idx, err := New(tmpIndexPath, mp) + if err != nil { + t.Fatal(err) + } + + batch := idx.NewBatch() + + docs := []map[string]interface{}{ + { + "textField": "ab", + }, + { + "textField": "abc", + }, + { + "textField": "abcd", + }, + } + + for _, doc := range docs { + err := batch.Index(fmt.Sprintf("%v", doc["textField"]), doc) + if err != nil { + t.Fatal(err) + } + } + + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + query := NewFuzzyQuery("ab") + query.Fuzziness = 2 + searchRequest := NewSearchRequestOptions(query, 10, 0, true) + res, err := idx.Search(searchRequest) + if err != nil { + t.Error(err) + } + + maxScore := res.Hits[0].Score + + for i, hit := range res.Hits { + if maxScore/float64(i+1) != hit.Score { + t.Errorf("expected score - %f, got score - %f", maxScore/float64(i+1), hit.Score) + } + } + + err = idx.Close() + if err != nil { + t.Fatal(err) + } +} diff --git a/mapping.go b/mapping.go new file mode 100644 index 0000000..723105a --- /dev/null +++ b/mapping.go @@ -0,0 +1,79 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import "github.com/blevesearch/bleve/v2/mapping" + +// NewIndexMapping creates a new IndexMapping that will use all the default indexing rules +func NewIndexMapping() *mapping.IndexMappingImpl { + return mapping.NewIndexMapping() +} + +// NewDocumentMapping returns a new document mapping +// with all the default values. +func NewDocumentMapping() *mapping.DocumentMapping { + return mapping.NewDocumentMapping() +} + +// NewDocumentStaticMapping returns a new document +// mapping that will not automatically index parts +// of a document without an explicit mapping. +func NewDocumentStaticMapping() *mapping.DocumentMapping { + return mapping.NewDocumentStaticMapping() +} + +// NewDocumentDisabledMapping returns a new document +// mapping that will not perform any indexing. +func NewDocumentDisabledMapping() *mapping.DocumentMapping { + return mapping.NewDocumentDisabledMapping() +} + +// NewTextFieldMapping returns a default field mapping for text +func NewTextFieldMapping() *mapping.FieldMapping { + return mapping.NewTextFieldMapping() +} + +// NewKeywordFieldMapping returns a field mapping for text using the keyword +// analyzer, which essentially doesn't apply any specific text analysis. +func NewKeywordFieldMapping() *mapping.FieldMapping { + return mapping.NewKeywordFieldMapping() +} + +// NewNumericFieldMapping returns a default field mapping for numbers +func NewNumericFieldMapping() *mapping.FieldMapping { + return mapping.NewNumericFieldMapping() +} + +// NewDateTimeFieldMapping returns a default field mapping for dates +func NewDateTimeFieldMapping() *mapping.FieldMapping { + return mapping.NewDateTimeFieldMapping() +} + +// NewBooleanFieldMapping returns a default field mapping for booleans +func NewBooleanFieldMapping() *mapping.FieldMapping { + return mapping.NewBooleanFieldMapping() +} + +func NewGeoPointFieldMapping() *mapping.FieldMapping { + return mapping.NewGeoPointFieldMapping() +} + +func NewGeoShapeFieldMapping() *mapping.FieldMapping { + return mapping.NewGeoShapeFieldMapping() +} + +func NewIPFieldMapping() *mapping.FieldMapping { + return mapping.NewIPFieldMapping() +} diff --git a/mapping/analysis.go b/mapping/analysis.go new file mode 100644 index 0000000..311e972 --- /dev/null +++ b/mapping/analysis.go @@ -0,0 +1,107 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapping + +type customAnalysis struct { + CharFilters map[string]map[string]interface{} `json:"char_filters,omitempty"` + Tokenizers map[string]map[string]interface{} `json:"tokenizers,omitempty"` + TokenMaps map[string]map[string]interface{} `json:"token_maps,omitempty"` + TokenFilters map[string]map[string]interface{} `json:"token_filters,omitempty"` + Analyzers map[string]map[string]interface{} `json:"analyzers,omitempty"` + DateTimeParsers map[string]map[string]interface{} `json:"date_time_parsers,omitempty"` + SynonymSources map[string]map[string]interface{} `json:"synonym_sources,omitempty"` +} + +func (c *customAnalysis) registerAll(i *IndexMappingImpl) error { + for name, config := range c.CharFilters { + _, err := i.cache.DefineCharFilter(name, config) + if err != nil { + return err + } + } + + if len(c.Tokenizers) > 0 { + // put all the names in map tracking work to do + todo := map[string]struct{}{} + for name := range c.Tokenizers { + todo[name] = struct{}{} + } + registered := 1 + errs := []error{} + // as long as we keep making progress, keep going + for len(todo) > 0 && registered > 0 { + registered = 0 + errs = []error{} + for name := range todo { + config := c.Tokenizers[name] + _, err := i.cache.DefineTokenizer(name, config) + if err != nil { + errs = append(errs, err) + } else { + delete(todo, name) + registered++ + } + } + } + + if len(errs) > 0 { + return errs[0] + } + } + for name, config := range c.TokenMaps { + _, err := i.cache.DefineTokenMap(name, config) + if err != nil { + return err + } + } + for name, config := range c.TokenFilters { + _, err := i.cache.DefineTokenFilter(name, config) + if err != nil { + return err + } + } + for name, config := range c.Analyzers { + _, err := i.cache.DefineAnalyzer(name, config) + if err != nil { + return err + } + } + for name, config := range c.DateTimeParsers { + _, err := i.cache.DefineDateTimeParser(name, config) + if err != nil { + return err + } + } + for name, config := range c.SynonymSources { + _, err := i.cache.DefineSynonymSource(name, config) + if err != nil { + return err + } + } + return nil +} + +func newCustomAnalysis() *customAnalysis { + rv := customAnalysis{ + CharFilters: make(map[string]map[string]interface{}), + Tokenizers: make(map[string]map[string]interface{}), + TokenMaps: make(map[string]map[string]interface{}), + TokenFilters: make(map[string]map[string]interface{}), + Analyzers: make(map[string]map[string]interface{}), + DateTimeParsers: make(map[string]map[string]interface{}), + SynonymSources: make(map[string]map[string]interface{}), + } + return &rv +} diff --git a/mapping/document.go b/mapping/document.go new file mode 100644 index 0000000..bf93896 --- /dev/null +++ b/mapping/document.go @@ -0,0 +1,646 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapping + +import ( + "encoding" + "encoding/json" + "fmt" + "net" + "reflect" + "time" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/util" +) + +// A DocumentMapping describes how a type of document +// should be indexed. +// As documents can be hierarchical, named sub-sections +// of documents are mapped using the same structure in +// the Properties field. +// Each value inside a document can be indexed 0 or more +// ways. These index entries are called fields and +// are stored in the Fields field. +// Entire sections of a document can be ignored or +// excluded by setting Enabled to false. +// If not explicitly mapped, default mapping operations +// are used. To disable this automatic handling, set +// Dynamic to false. +type DocumentMapping struct { + Enabled bool `json:"enabled"` + Dynamic bool `json:"dynamic"` + Properties map[string]*DocumentMapping `json:"properties,omitempty"` + Fields []*FieldMapping `json:"fields,omitempty"` + DefaultAnalyzer string `json:"default_analyzer,omitempty"` + DefaultSynonymSource string `json:"default_synonym_source,omitempty"` + + // StructTagKey overrides "json" when looking for field names in struct tags + StructTagKey string `json:"struct_tag_key,omitempty"` +} + +func (dm *DocumentMapping) Validate(cache *registry.Cache, + parentName string, fieldAliasCtx map[string]*FieldMapping, +) error { + var err error + if dm.DefaultAnalyzer != "" { + _, err := cache.AnalyzerNamed(dm.DefaultAnalyzer) + if err != nil { + return err + } + } + if dm.DefaultSynonymSource != "" { + _, err := cache.SynonymSourceNamed(dm.DefaultSynonymSource) + if err != nil { + return err + } + } + for propertyName, property := range dm.Properties { + newParent := propertyName + if parentName != "" { + newParent = fmt.Sprintf("%s.%s", parentName, propertyName) + } + err = property.Validate(cache, newParent, fieldAliasCtx) + if err != nil { + return err + } + } + for _, field := range dm.Fields { + if field.Analyzer != "" { + _, err = cache.AnalyzerNamed(field.Analyzer) + if err != nil { + return err + } + } + if field.DateFormat != "" { + _, err = cache.DateTimeParserNamed(field.DateFormat) + if err != nil { + return err + } + } + if field.SynonymSource != "" { + _, err = cache.SynonymSourceNamed(field.SynonymSource) + if err != nil { + return err + } + } + err := validateFieldMapping(field, parentName, fieldAliasCtx) + if err != nil { + return err + } + } + return nil +} + +func validateFieldType(field *FieldMapping) error { + switch field.Type { + case "text", "datetime", "number", "boolean", "geopoint", "geoshape", "IP": + return nil + default: + return fmt.Errorf("field: '%s', unknown field type: '%s'", + field.Name, field.Type) + } +} + +// analyzerNameForPath attempts to first find the field +// described by this path, then returns the analyzer +// configured for that field +func (dm *DocumentMapping) analyzerNameForPath(path string) string { + field := dm.fieldDescribedByPath(path) + if field != nil { + return field.Analyzer + } + return "" +} + +// synonymSourceForPath attempts to first find the field +// described by this path, then returns the analyzer +// configured for that field +func (dm *DocumentMapping) synonymSourceForPath(path string) string { + field := dm.fieldDescribedByPath(path) + if field != nil { + return field.SynonymSource + } + return "" +} + +func (dm *DocumentMapping) fieldDescribedByPath(path string) *FieldMapping { + pathElements := decodePath(path) + if len(pathElements) > 1 { + // easy case, there is more than 1 path element remaining + // the next path element must match a property name + // at this level + for propName, subDocMapping := range dm.Properties { + if propName == pathElements[0] { + return subDocMapping.fieldDescribedByPath(encodePath(pathElements[1:])) + } + } + } + + // either the path just had one element + // or it had multiple, but no match for the first element at this level + // look for match with full path + + // first look for property name with empty field + for propName, subDocMapping := range dm.Properties { + if propName == path { + // found property name match, now look at its fields + for _, field := range subDocMapping.Fields { + if field.Name == "" || field.Name == path { + // match + return field + } + } + } + } + // next, walk the properties again, looking for field overriding the name + for propName, subDocMapping := range dm.Properties { + if propName != path { + // property name isn't a match, but field name could override it + for _, field := range subDocMapping.Fields { + if field.Name == path { + return field + } + } + } + } + + return nil +} + +// documentMappingForPathElements returns the EXACT and closest matches for a sub +// document or for an explicitly mapped field; the closest most specific +// document mapping could be one that matches part of the provided path. +func (dm *DocumentMapping) documentMappingForPathElements(pathElements []string) ( + *DocumentMapping, *DocumentMapping, +) { + var pathElementsCopy []string + if len(pathElements) == 0 { + pathElementsCopy = []string{""} + } else { + pathElementsCopy = pathElements + } + current := dm +OUTER: + for i, pathElement := range pathElementsCopy { + if subDocMapping, exists := current.Properties[pathElement]; exists { + current = subDocMapping + continue OUTER + } + + // no subDocMapping matches this pathElement + // only if this is the last element check for field name + if i == len(pathElementsCopy)-1 { + for _, field := range current.Fields { + if field.Name == pathElement { + break + } + } + } + + return nil, current + } + return current, current +} + +// documentMappingForPath returns the EXACT and closest matches for a sub +// document or for an explicitly mapped field; the closest most specific +// document mapping could be one that matches part of the provided path. +func (dm *DocumentMapping) documentMappingForPath(path string) ( + *DocumentMapping, *DocumentMapping, +) { + pathElements := decodePath(path) + return dm.documentMappingForPathElements(pathElements) +} + +// NewDocumentMapping returns a new document mapping +// with all the default values. +func NewDocumentMapping() *DocumentMapping { + return &DocumentMapping{ + Enabled: true, + Dynamic: true, + } +} + +// NewDocumentStaticMapping returns a new document +// mapping that will not automatically index parts +// of a document without an explicit mapping. +func NewDocumentStaticMapping() *DocumentMapping { + return &DocumentMapping{ + Enabled: true, + } +} + +// NewDocumentDisabledMapping returns a new document +// mapping that will not perform any indexing. +func NewDocumentDisabledMapping() *DocumentMapping { + return &DocumentMapping{} +} + +// AddSubDocumentMapping adds the provided DocumentMapping as a sub-mapping +// for the specified named subsection. +func (dm *DocumentMapping) AddSubDocumentMapping(property string, sdm *DocumentMapping) { + if dm.Properties == nil { + dm.Properties = make(map[string]*DocumentMapping) + } + dm.Properties[property] = sdm +} + +// AddFieldMappingsAt adds one or more FieldMappings +// at the named sub-document. If the named sub-document +// doesn't yet exist it is created for you. +// This is a convenience function to make most common +// mappings more concise. +// Otherwise, you would: +// +// subMapping := NewDocumentMapping() +// subMapping.AddFieldMapping(fieldMapping) +// parentMapping.AddSubDocumentMapping(property, subMapping) +func (dm *DocumentMapping) AddFieldMappingsAt(property string, fms ...*FieldMapping) { + if dm.Properties == nil { + dm.Properties = make(map[string]*DocumentMapping) + } + sdm, ok := dm.Properties[property] + if !ok { + sdm = NewDocumentMapping() + } + for _, fm := range fms { + sdm.AddFieldMapping(fm) + } + dm.Properties[property] = sdm +} + +// AddFieldMapping adds the provided FieldMapping for this section +// of the document. +func (dm *DocumentMapping) AddFieldMapping(fm *FieldMapping) { + if dm.Fields == nil { + dm.Fields = make([]*FieldMapping, 0) + } + dm.Fields = append(dm.Fields, fm) +} + +// UnmarshalJSON offers custom unmarshaling with optional strict validation +func (dm *DocumentMapping) UnmarshalJSON(data []byte) error { + var tmp map[string]json.RawMessage + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + + // set defaults for fields which might have been omitted + dm.Enabled = true + dm.Dynamic = true + + var invalidKeys []string + for k, v := range tmp { + switch k { + case "enabled": + err := util.UnmarshalJSON(v, &dm.Enabled) + if err != nil { + return err + } + case "dynamic": + err := util.UnmarshalJSON(v, &dm.Dynamic) + if err != nil { + return err + } + case "default_analyzer": + err := util.UnmarshalJSON(v, &dm.DefaultAnalyzer) + if err != nil { + return err + } + case "default_synonym_source": + err := util.UnmarshalJSON(v, &dm.DefaultSynonymSource) + if err != nil { + return err + } + case "properties": + err := util.UnmarshalJSON(v, &dm.Properties) + if err != nil { + return err + } + case "fields": + err := util.UnmarshalJSON(v, &dm.Fields) + if err != nil { + return err + } + case "struct_tag_key": + err := util.UnmarshalJSON(v, &dm.StructTagKey) + if err != nil { + return err + } + default: + invalidKeys = append(invalidKeys, k) + } + } + + if MappingJSONStrict && len(invalidKeys) > 0 { + return fmt.Errorf("document mapping contains invalid keys: %v", invalidKeys) + } + + return nil +} + +func (dm *DocumentMapping) defaultAnalyzerName(path []string) string { + current := dm + rv := current.DefaultAnalyzer + for _, pathElement := range path { + var ok bool + current, ok = current.Properties[pathElement] + if !ok { + break + } + if current.DefaultAnalyzer != "" { + rv = current.DefaultAnalyzer + } + } + return rv +} + +func (dm *DocumentMapping) defaultSynonymSource(path []string) string { + current := dm + rv := current.DefaultSynonymSource + for _, pathElement := range path { + var ok bool + current, ok = current.Properties[pathElement] + if !ok { + break + } + if current.DefaultSynonymSource != "" { + rv = current.DefaultSynonymSource + } + } + return rv +} + +func (dm *DocumentMapping) walkDocument(data interface{}, path []string, indexes []uint64, context *walkContext) { + // allow default "json" tag to be overridden + structTagKey := dm.StructTagKey + if structTagKey == "" { + structTagKey = "json" + } + + val := reflect.ValueOf(data) + if !val.IsValid() { + return + } + + typ := val.Type() + switch typ.Kind() { + case reflect.Map: + // FIXME can add support for other map keys in the future + if typ.Key().Kind() == reflect.String { + for _, key := range val.MapKeys() { + fieldName := key.String() + fieldVal := val.MapIndex(key).Interface() + dm.processProperty(fieldVal, append(path, fieldName), indexes, context) + } + } + case reflect.Struct: + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + fieldName := field.Name + // anonymous fields of type struct can elide the type name + if field.Anonymous && field.Type.Kind() == reflect.Struct { + fieldName = "" + } + + // if the field has a name under the specified tag, prefer that + tag := field.Tag.Get(structTagKey) + tagFieldName := parseTagName(tag) + if tagFieldName == "-" { + continue + } + // allow tag to set field name to empty, only if anonymous + if field.Tag != "" && (tagFieldName != "" || field.Anonymous) { + fieldName = tagFieldName + } + + if val.Field(i).CanInterface() { + fieldVal := val.Field(i).Interface() + newpath := path + if fieldName != "" { + newpath = append(path, fieldName) + } + dm.processProperty(fieldVal, newpath, indexes, context) + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + if val.Index(i).CanInterface() { + fieldVal := val.Index(i).Interface() + dm.processProperty(fieldVal, path, append(indexes, uint64(i)), context) + } + } + case reflect.Ptr: + ptrElem := val.Elem() + if ptrElem.IsValid() && ptrElem.CanInterface() { + dm.processProperty(ptrElem.Interface(), path, indexes, context) + } + case reflect.String: + dm.processProperty(val.String(), path, indexes, context) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + dm.processProperty(float64(val.Int()), path, indexes, context) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + dm.processProperty(float64(val.Uint()), path, indexes, context) + case reflect.Float32, reflect.Float64: + dm.processProperty(float64(val.Float()), path, indexes, context) + case reflect.Bool: + dm.processProperty(val.Bool(), path, indexes, context) + } +} + +func (dm *DocumentMapping) processProperty(property interface{}, path []string, indexes []uint64, context *walkContext) { + // look to see if there is a mapping for this field + subDocMapping, closestDocMapping := dm.documentMappingForPathElements(path) + + // check to see if we even need to do further processing + if subDocMapping != nil && !subDocMapping.Enabled { + return + } + + propertyValue := reflect.ValueOf(property) + if !propertyValue.IsValid() { + // cannot do anything with the zero value + return + } + + pathString := encodePath(path) + propertyType := propertyValue.Type() + switch propertyType.Kind() { + case reflect.String: + propertyValueString := propertyValue.String() + if subDocMapping != nil { + // index by explicit mapping + for _, fieldMapping := range subDocMapping.Fields { + switch fieldMapping.Type { + case "geoshape": + fieldMapping.processGeoShape(property, pathString, path, indexes, context) + case "geopoint": + fieldMapping.processGeoPoint(property, pathString, path, indexes, context) + case "vector_base64": + fieldMapping.processVectorBase64(property, pathString, path, indexes, context) + default: + fieldMapping.processString(propertyValueString, pathString, path, indexes, context) + } + } + } else if closestDocMapping.Dynamic { + // automatic indexing behavior + + // first see if it can be parsed by the default date parser + dateTimeParser := context.im.DateTimeParserNamed(context.im.DefaultDateTimeParser) + if dateTimeParser != nil { + parsedDateTime, layout, err := dateTimeParser.ParseDateTime(propertyValueString) + if err != nil { + // index as text + fieldMapping := newTextFieldMappingDynamic(context.im) + fieldMapping.processString(propertyValueString, pathString, path, indexes, context) + } else { + // index as datetime + fieldMapping := newDateTimeFieldMappingDynamic(context.im) + fieldMapping.processTime(parsedDateTime, layout, pathString, path, indexes, context) + } + } + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + dm.processProperty(float64(propertyValue.Int()), path, indexes, context) + return + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + dm.processProperty(float64(propertyValue.Uint()), path, indexes, context) + return + case reflect.Float64, reflect.Float32: + propertyValFloat := propertyValue.Float() + if subDocMapping != nil { + // index by explicit mapping + for _, fieldMapping := range subDocMapping.Fields { + fieldMapping.processFloat64(propertyValFloat, pathString, path, indexes, context) + } + } else if closestDocMapping.Dynamic { + // automatic indexing behavior + fieldMapping := newNumericFieldMappingDynamic(context.im) + fieldMapping.processFloat64(propertyValFloat, pathString, path, indexes, context) + } + case reflect.Bool: + propertyValBool := propertyValue.Bool() + if subDocMapping != nil { + // index by explicit mapping + for _, fieldMapping := range subDocMapping.Fields { + fieldMapping.processBoolean(propertyValBool, pathString, path, indexes, context) + } + } else if closestDocMapping.Dynamic { + // automatic indexing behavior + fieldMapping := newBooleanFieldMappingDynamic(context.im) + fieldMapping.processBoolean(propertyValBool, pathString, path, indexes, context) + } + case reflect.Struct: + switch property := property.(type) { + case time.Time: + // don't descend into the time struct + if subDocMapping != nil { + // index by explicit mapping + for _, fieldMapping := range subDocMapping.Fields { + fieldMapping.processTime(property, time.RFC3339, pathString, path, indexes, context) + } + } else if closestDocMapping.Dynamic { + fieldMapping := newDateTimeFieldMappingDynamic(context.im) + fieldMapping.processTime(property, time.RFC3339, pathString, path, indexes, context) + } + case encoding.TextMarshaler: + txt, err := property.MarshalText() + if err == nil && subDocMapping != nil { + // index by explicit mapping + for _, fieldMapping := range subDocMapping.Fields { + if fieldMapping.Type == "text" { + fieldMapping.processString(string(txt), pathString, path, indexes, context) + } + } + } + dm.walkDocument(property, path, indexes, context) + default: + if subDocMapping != nil { + for _, fieldMapping := range subDocMapping.Fields { + switch fieldMapping.Type { + case "geopoint": + fieldMapping.processGeoPoint(property, pathString, path, indexes, context) + case "geoshape": + fieldMapping.processGeoShape(property, pathString, path, indexes, context) + } + } + } + dm.walkDocument(property, path, indexes, context) + } + case reflect.Map, reflect.Slice: + walkDocument := false + if subDocMapping != nil && len(subDocMapping.Fields) != 0 { + for _, fieldMapping := range subDocMapping.Fields { + switch fieldMapping.Type { + case "vector": + fieldMapping.processVector(property, pathString, path, + indexes, context) + case "geopoint": + fieldMapping.processGeoPoint(property, pathString, path, indexes, context) + walkDocument = true + case "IP": + ip, ok := property.(net.IP) + if ok { + fieldMapping.processIP(ip, pathString, path, indexes, context) + } + walkDocument = true + case "geoshape": + fieldMapping.processGeoShape(property, pathString, path, indexes, context) + walkDocument = true + default: + walkDocument = true + } + } + } else { + walkDocument = true + } + if walkDocument { + dm.walkDocument(property, path, indexes, context) + } + case reflect.Ptr: + if !propertyValue.IsNil() { + switch property := property.(type) { + case encoding.TextMarshaler: + // ONLY process TextMarshaler if there is an explicit mapping + // AND all of the fields are of type text + // OTHERWISE process field without TextMarshaler + if subDocMapping != nil { + allFieldsText := true + for _, fieldMapping := range subDocMapping.Fields { + if fieldMapping.Type != "text" { + allFieldsText = false + break + } + } + txt, err := property.MarshalText() + if err == nil && allFieldsText { + txtStr := string(txt) + for _, fieldMapping := range subDocMapping.Fields { + fieldMapping.processString(txtStr, pathString, path, indexes, context) + } + return + } + } + dm.walkDocument(property, path, indexes, context) + default: + dm.walkDocument(property, path, indexes, context) + } + } + default: + dm.walkDocument(property, path, indexes, context) + } +} diff --git a/mapping/examples_test.go b/mapping/examples_test.go new file mode 100644 index 0000000..c45e1ae --- /dev/null +++ b/mapping/examples_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapping + +import "fmt" + +// Examples for Mapping related functions + +func ExampleDocumentMapping_AddSubDocumentMapping() { + // adds a document mapping for a property in a document + // useful for mapping nested documents + documentMapping := NewDocumentMapping() + subDocumentMapping := NewDocumentMapping() + documentMapping.AddSubDocumentMapping("Property", subDocumentMapping) + + fmt.Println(len(documentMapping.Properties)) + // Output: + // 1 +} + +func ExampleDocumentMapping_AddFieldMapping() { + // you can only add field mapping to those properties which already have a document mapping + documentMapping := NewDocumentMapping() + subDocumentMapping := NewDocumentMapping() + documentMapping.AddSubDocumentMapping("Property", subDocumentMapping) + + fieldMapping := NewTextFieldMapping() + fieldMapping.Analyzer = "en" + subDocumentMapping.AddFieldMapping(fieldMapping) + + fmt.Println(len(documentMapping.Properties["Property"].Fields)) + // Output: + // 1 +} + +func ExampleDocumentMapping_AddFieldMappingsAt() { + // you can only add field mapping to those properties which already have a document mapping + documentMapping := NewDocumentMapping() + subDocumentMapping := NewDocumentMapping() + documentMapping.AddSubDocumentMapping("NestedProperty", subDocumentMapping) + + fieldMapping := NewTextFieldMapping() + fieldMapping.Analyzer = "en" + documentMapping.AddFieldMappingsAt("NestedProperty", fieldMapping) + + fmt.Println(len(documentMapping.Properties["NestedProperty"].Fields)) + // Output: + // 1 +} diff --git a/mapping/field.go b/mapping/field.go new file mode 100644 index 0000000..0b60749 --- /dev/null +++ b/mapping/field.go @@ -0,0 +1,492 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapping + +import ( + "encoding/json" + "fmt" + "net" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" + "github.com/blevesearch/geo/geojson" +) + +// control the default behavior for dynamic fields (those not explicitly mapped) +var ( + IndexDynamic = true + StoreDynamic = true + DocValuesDynamic = true // TODO revisit default? +) + +// A FieldMapping describes how a specific item +// should be put into the index. +type FieldMapping struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + + // Analyzer specifies the name of the analyzer to use for this field. If + // Analyzer is empty, traverse the DocumentMapping tree toward the root and + // pick the first non-empty DefaultAnalyzer found. If there is none, use + // the IndexMapping.DefaultAnalyzer. + Analyzer string `json:"analyzer,omitempty"` + + // Store indicates whether to store field values in the index. Stored + // values can be retrieved from search results using SearchRequest.Fields. + Store bool `json:"store,omitempty"` + Index bool `json:"index,omitempty"` + + // IncludeTermVectors, if true, makes terms occurrences to be recorded for + // this field. It includes the term position within the terms sequence and + // the term offsets in the source document field. Term vectors are required + // to perform phrase queries or terms highlighting in source documents. + IncludeTermVectors bool `json:"include_term_vectors,omitempty"` + IncludeInAll bool `json:"include_in_all,omitempty"` + DateFormat string `json:"date_format,omitempty"` + + // DocValues, if true makes the index uninverting possible for this field + // It is useful for faceting and sorting queries. + DocValues bool `json:"docvalues,omitempty"` + + // SkipFreqNorm, if true, avoids the indexing of frequency and norm values + // of the tokens for this field. This option would be useful for saving + // the processing of freq/norm details when the default score based relevancy + // isn't needed. + SkipFreqNorm bool `json:"skip_freq_norm,omitempty"` + + // Dimensionality of the vector + Dims int `json:"dims,omitempty"` + + // Similarity is the similarity algorithm used for scoring + // field's content while performing search on it. + // See: index.SimilarityModels + Similarity string `json:"similarity,omitempty"` + + // Applicable to vector fields only - optimization string + VectorIndexOptimizedFor string `json:"vector_index_optimized_for,omitempty"` + + SynonymSource string `json:"synonym_source,omitempty"` +} + +// NewTextFieldMapping returns a default field mapping for text +func NewTextFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "text", + Store: true, + Index: true, + IncludeTermVectors: true, + IncludeInAll: true, + DocValues: true, + } +} + +func newTextFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping { + rv := NewTextFieldMapping() + rv.Store = im.StoreDynamic + rv.Index = im.IndexDynamic + rv.DocValues = im.DocValuesDynamic + return rv +} + +// NewKeywordFieldMapping returns a default field mapping for text with analyzer "keyword". +func NewKeywordFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "text", + Analyzer: keyword.Name, + Store: true, + Index: true, + IncludeTermVectors: true, + IncludeInAll: true, + DocValues: true, + } +} + +// NewNumericFieldMapping returns a default field mapping for numbers +func NewNumericFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "number", + Store: true, + Index: true, + IncludeInAll: true, + DocValues: true, + } +} + +func newNumericFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping { + rv := NewNumericFieldMapping() + rv.Store = im.StoreDynamic + rv.Index = im.IndexDynamic + rv.DocValues = im.DocValuesDynamic + return rv +} + +// NewDateTimeFieldMapping returns a default field mapping for dates +func NewDateTimeFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "datetime", + Store: true, + Index: true, + IncludeInAll: true, + DocValues: true, + } +} + +func newDateTimeFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping { + rv := NewDateTimeFieldMapping() + rv.Store = im.StoreDynamic + rv.Index = im.IndexDynamic + rv.DocValues = im.DocValuesDynamic + return rv +} + +// NewBooleanFieldMapping returns a default field mapping for booleans +func NewBooleanFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "boolean", + Store: true, + Index: true, + IncludeInAll: true, + DocValues: true, + } +} + +func newBooleanFieldMappingDynamic(im *IndexMappingImpl) *FieldMapping { + rv := NewBooleanFieldMapping() + rv.Store = im.StoreDynamic + rv.Index = im.IndexDynamic + rv.DocValues = im.DocValuesDynamic + return rv +} + +// NewGeoPointFieldMapping returns a default field mapping for geo points +func NewGeoPointFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "geopoint", + Store: true, + Index: true, + IncludeInAll: true, + DocValues: true, + } +} + +// NewGeoShapeFieldMapping returns a default field mapping +// for geoshapes +func NewGeoShapeFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "geoshape", + Store: true, + Index: true, + IncludeInAll: true, + DocValues: true, + } +} + +// NewIPFieldMapping returns a default field mapping for IP points +func NewIPFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "IP", + Store: true, + Index: true, + IncludeInAll: true, + } +} + +// Options returns the indexing options for this field. +func (fm *FieldMapping) Options() index.FieldIndexingOptions { + var rv index.FieldIndexingOptions + if fm.Store { + rv |= index.StoreField + } + if fm.Index { + rv |= index.IndexField + } + if fm.IncludeTermVectors { + rv |= index.IncludeTermVectors + } + if fm.DocValues { + rv |= index.DocValues + } + if fm.SkipFreqNorm { + rv |= index.SkipFreqNorm + } + return rv +} + +func (fm *FieldMapping) processString(propertyValueString string, pathString string, path []string, indexes []uint64, context *walkContext) { + fieldName := getFieldName(pathString, path, fm) + options := fm.Options() + + switch fm.Type { + case "text": + analyzer := fm.analyzerForField(path, context) + field := document.NewTextFieldCustom(fieldName, indexes, []byte(propertyValueString), options, analyzer) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + case "datetime": + dateTimeFormat := context.im.DefaultDateTimeParser + if fm.DateFormat != "" { + dateTimeFormat = fm.DateFormat + } + dateTimeParser := context.im.DateTimeParserNamed(dateTimeFormat) + if dateTimeParser != nil { + parsedDateTime, layout, err := dateTimeParser.ParseDateTime(propertyValueString) + if err == nil { + fm.processTime(parsedDateTime, layout, pathString, path, indexes, context) + } + } + case "IP": + ip := net.ParseIP(propertyValueString) + if ip != nil { + fm.processIP(ip, pathString, path, indexes, context) + } + } +} + +func (fm *FieldMapping) processFloat64(propertyValFloat float64, pathString string, path []string, indexes []uint64, context *walkContext) { + fieldName := getFieldName(pathString, path, fm) + if fm.Type == "number" { + options := fm.Options() + field := document.NewNumericFieldWithIndexingOptions(fieldName, indexes, propertyValFloat, options) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + } +} + +func (fm *FieldMapping) processTime(propertyValueTime time.Time, layout string, pathString string, path []string, indexes []uint64, context *walkContext) { + fieldName := getFieldName(pathString, path, fm) + if fm.Type == "datetime" { + options := fm.Options() + field, err := document.NewDateTimeFieldWithIndexingOptions(fieldName, indexes, propertyValueTime, layout, options) + if err == nil { + context.doc.AddField(field) + } else { + logger.Printf("could not build date %v", err) + } + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + } +} + +func (fm *FieldMapping) processBoolean(propertyValueBool bool, pathString string, path []string, indexes []uint64, context *walkContext) { + fieldName := getFieldName(pathString, path, fm) + if fm.Type == "boolean" { + options := fm.Options() + field := document.NewBooleanFieldWithIndexingOptions(fieldName, indexes, propertyValueBool, options) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + } +} + +func (fm *FieldMapping) processGeoPoint(propertyMightBeGeoPoint interface{}, pathString string, path []string, indexes []uint64, context *walkContext) { + lon, lat, found := geo.ExtractGeoPoint(propertyMightBeGeoPoint) + if found { + fieldName := getFieldName(pathString, path, fm) + options := fm.Options() + field := document.NewGeoPointFieldWithIndexingOptions(fieldName, indexes, lon, lat, options) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + } +} + +func (fm *FieldMapping) processIP(ip net.IP, pathString string, path []string, indexes []uint64, context *walkContext) { + fieldName := getFieldName(pathString, path, fm) + options := fm.Options() + field := document.NewIPFieldWithIndexingOptions(fieldName, indexes, ip, options) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } +} + +func (fm *FieldMapping) processGeoShape(propertyMightBeGeoShape interface{}, + pathString string, path []string, indexes []uint64, context *walkContext, +) { + coordValue, shape, err := geo.ParseGeoShapeField(propertyMightBeGeoShape) + if err != nil { + return + } + + if shape == geo.GeometryCollectionType { + geoShapes, found := geo.ExtractGeometryCollection(propertyMightBeGeoShape) + if found { + fieldName := getFieldName(pathString, path, fm) + options := fm.Options() + field := document.NewGeometryCollectionFieldFromShapesWithIndexingOptions(fieldName, + indexes, geoShapes, options) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + } + } else { + var geoShape *geojson.GeoShape + var found bool + + if shape == geo.CircleType { + geoShape, found = geo.ExtractCircle(propertyMightBeGeoShape) + } else { + geoShape, found = geo.ExtractGeoShapeCoordinates(coordValue, shape) + } + + if found { + fieldName := getFieldName(pathString, path, fm) + options := fm.Options() + field := document.NewGeoShapeFieldFromShapeWithIndexingOptions(fieldName, + indexes, geoShape, options) + context.doc.AddField(field) + + if !fm.IncludeInAll { + context.excludedFromAll = append(context.excludedFromAll, fieldName) + } + } + } +} + +func (fm *FieldMapping) analyzerForField(path []string, context *walkContext) analysis.Analyzer { + analyzerName := fm.Analyzer + if analyzerName == "" { + analyzerName = context.dm.defaultAnalyzerName(path) + if analyzerName == "" { + analyzerName = context.im.DefaultAnalyzer + } + } + return context.im.AnalyzerNamed(analyzerName) +} + +func getFieldName(pathString string, path []string, fieldMapping *FieldMapping) string { + fieldName := pathString + if fieldMapping.Name != "" { + parentName := "" + if len(path) > 1 { + parentName = encodePath(path[:len(path)-1]) + pathSeparator + } + fieldName = parentName + fieldMapping.Name + } + return fieldName +} + +// UnmarshalJSON offers custom unmarshaling with optional strict validation +func (fm *FieldMapping) UnmarshalJSON(data []byte) error { + var tmp map[string]json.RawMessage + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + + var invalidKeys []string + for k, v := range tmp { + switch k { + case "name": + err := util.UnmarshalJSON(v, &fm.Name) + if err != nil { + return err + } + case "type": + err := util.UnmarshalJSON(v, &fm.Type) + if err != nil { + return err + } + case "analyzer": + err := util.UnmarshalJSON(v, &fm.Analyzer) + if err != nil { + return err + } + case "store": + err := util.UnmarshalJSON(v, &fm.Store) + if err != nil { + return err + } + case "index": + err := util.UnmarshalJSON(v, &fm.Index) + if err != nil { + return err + } + case "include_term_vectors": + err := util.UnmarshalJSON(v, &fm.IncludeTermVectors) + if err != nil { + return err + } + case "include_in_all": + err := util.UnmarshalJSON(v, &fm.IncludeInAll) + if err != nil { + return err + } + case "date_format": + err := util.UnmarshalJSON(v, &fm.DateFormat) + if err != nil { + return err + } + case "docvalues": + err := util.UnmarshalJSON(v, &fm.DocValues) + if err != nil { + return err + } + case "skip_freq_norm": + err := util.UnmarshalJSON(v, &fm.SkipFreqNorm) + if err != nil { + return err + } + case "dims": + err := util.UnmarshalJSON(v, &fm.Dims) + if err != nil { + return err + } + case "similarity": + err := util.UnmarshalJSON(v, &fm.Similarity) + if err != nil { + return err + } + case "vector_index_optimized_for": + err := util.UnmarshalJSON(v, &fm.VectorIndexOptimizedFor) + if err != nil { + return err + } + case "synonym_source": + err := util.UnmarshalJSON(v, &fm.SynonymSource) + if err != nil { + return err + } + default: + invalidKeys = append(invalidKeys, k) + } + } + + if MappingJSONStrict && len(invalidKeys) > 0 { + return fmt.Errorf("field mapping contains invalid keys: %v", invalidKeys) + } + + return nil +} diff --git a/mapping/index.go b/mapping/index.go new file mode 100644 index 0000000..a40feb4 --- /dev/null +++ b/mapping/index.go @@ -0,0 +1,573 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapping + +import ( + "encoding/json" + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + "github.com/blevesearch/bleve/v2/analysis/datetime/optional" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +var MappingJSONStrict = false + +const defaultTypeField = "_type" +const defaultType = "_default" +const defaultField = "_all" +const defaultAnalyzer = standard.Name +const defaultDateTimeParser = optional.Name + +// An IndexMappingImpl controls how objects are placed +// into an index. +// First the type of the object is determined. +// Once the type is know, the appropriate +// DocumentMapping is selected by the type. +// If no mapping was determined for that type, +// a DefaultMapping will be used. +type IndexMappingImpl struct { + TypeMapping map[string]*DocumentMapping `json:"types,omitempty"` + DefaultMapping *DocumentMapping `json:"default_mapping"` + TypeField string `json:"type_field"` + DefaultType string `json:"default_type"` + DefaultAnalyzer string `json:"default_analyzer"` + DefaultDateTimeParser string `json:"default_datetime_parser"` + DefaultSynonymSource string `json:"default_synonym_source,omitempty"` + ScoringModel string `json:"scoring_model,omitempty"` + DefaultField string `json:"default_field"` + StoreDynamic bool `json:"store_dynamic"` + IndexDynamic bool `json:"index_dynamic"` + DocValuesDynamic bool `json:"docvalues_dynamic"` + CustomAnalysis *customAnalysis `json:"analysis,omitempty"` + cache *registry.Cache +} + +// AddCustomCharFilter defines a custom char filter for use in this mapping +func (im *IndexMappingImpl) AddCustomCharFilter(name string, config map[string]interface{}) error { + _, err := im.cache.DefineCharFilter(name, config) + if err != nil { + return err + } + im.CustomAnalysis.CharFilters[name] = config + return nil +} + +// AddCustomTokenizer defines a custom tokenizer for use in this mapping +func (im *IndexMappingImpl) AddCustomTokenizer(name string, config map[string]interface{}) error { + _, err := im.cache.DefineTokenizer(name, config) + if err != nil { + return err + } + im.CustomAnalysis.Tokenizers[name] = config + return nil +} + +// AddCustomTokenMap defines a custom token map for use in this mapping +func (im *IndexMappingImpl) AddCustomTokenMap(name string, config map[string]interface{}) error { + _, err := im.cache.DefineTokenMap(name, config) + if err != nil { + return err + } + im.CustomAnalysis.TokenMaps[name] = config + return nil +} + +// AddCustomTokenFilter defines a custom token filter for use in this mapping +func (im *IndexMappingImpl) AddCustomTokenFilter(name string, config map[string]interface{}) error { + _, err := im.cache.DefineTokenFilter(name, config) + if err != nil { + return err + } + im.CustomAnalysis.TokenFilters[name] = config + return nil +} + +// AddCustomAnalyzer defines a custom analyzer for use in this mapping. The +// config map must have a "type" string entry to resolve the analyzer +// constructor. The constructor is invoked with the remaining entries and +// returned analyzer is registered in the IndexMapping. +// +// bleve comes with predefined analyzers, like +// github.com/blevesearch/bleve/analysis/analyzer/custom. They are +// available only if their package is imported by client code. To achieve this, +// use their metadata to fill configuration entries: +// +// import ( +// "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" +// "github.com/blevesearch/bleve/v2/analysis/char/html" +// "github.com/blevesearch/bleve/v2/analysis/token/lowercase" +// "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" +// ) +// +// m := bleve.NewIndexMapping() +// err := m.AddCustomAnalyzer("html", map[string]interface{}{ +// "type": custom.Name, +// "char_filters": []string{ +// html.Name, +// }, +// "tokenizer": unicode.Name, +// "token_filters": []string{ +// lowercase.Name, +// ... +// }, +// }) +func (im *IndexMappingImpl) AddCustomAnalyzer(name string, config map[string]interface{}) error { + _, err := im.cache.DefineAnalyzer(name, config) + if err != nil { + return err + } + im.CustomAnalysis.Analyzers[name] = config + return nil +} + +// AddCustomDateTimeParser defines a custom date time parser for use in this mapping +func (im *IndexMappingImpl) AddCustomDateTimeParser(name string, config map[string]interface{}) error { + _, err := im.cache.DefineDateTimeParser(name, config) + if err != nil { + return err + } + im.CustomAnalysis.DateTimeParsers[name] = config + return nil +} + +func (im *IndexMappingImpl) AddSynonymSource(name string, config map[string]interface{}) error { + _, err := im.cache.DefineSynonymSource(name, config) + if err != nil { + return err + } + im.CustomAnalysis.SynonymSources[name] = config + return nil +} + +// NewIndexMapping creates a new IndexMapping that will use all the default indexing rules +func NewIndexMapping() *IndexMappingImpl { + return &IndexMappingImpl{ + TypeMapping: make(map[string]*DocumentMapping), + DefaultMapping: NewDocumentMapping(), + TypeField: defaultTypeField, + DefaultType: defaultType, + DefaultAnalyzer: defaultAnalyzer, + DefaultDateTimeParser: defaultDateTimeParser, + DefaultField: defaultField, + IndexDynamic: IndexDynamic, + StoreDynamic: StoreDynamic, + DocValuesDynamic: DocValuesDynamic, + CustomAnalysis: newCustomAnalysis(), + cache: registry.NewCache(), + } +} + +// Validate will walk the entire structure ensuring the following +// explicitly named and default analyzers can be built +func (im *IndexMappingImpl) Validate() error { + _, err := im.cache.AnalyzerNamed(im.DefaultAnalyzer) + if err != nil { + return err + } + _, err = im.cache.DateTimeParserNamed(im.DefaultDateTimeParser) + if err != nil { + return err + } + if im.DefaultSynonymSource != "" { + _, err = im.cache.SynonymSourceNamed(im.DefaultSynonymSource) + if err != nil { + return err + } + } + fieldAliasCtx := make(map[string]*FieldMapping) + err = im.DefaultMapping.Validate(im.cache, "", fieldAliasCtx) + if err != nil { + return err + } + for _, docMapping := range im.TypeMapping { + err = docMapping.Validate(im.cache, "", fieldAliasCtx) + if err != nil { + return err + } + } + + if _, ok := index.SupportedScoringModels[im.ScoringModel]; !ok && im.ScoringModel != "" { + return fmt.Errorf("unsupported scoring model: %s", im.ScoringModel) + } + + return nil +} + +// AddDocumentMapping sets a custom document mapping for the specified type +func (im *IndexMappingImpl) AddDocumentMapping(doctype string, dm *DocumentMapping) { + im.TypeMapping[doctype] = dm +} + +func (im *IndexMappingImpl) mappingForType(docType string) *DocumentMapping { + docMapping := im.TypeMapping[docType] + if docMapping == nil { + docMapping = im.DefaultMapping + } + return docMapping +} + +// UnmarshalJSON offers custom unmarshaling with optional strict validation +func (im *IndexMappingImpl) UnmarshalJSON(data []byte) error { + + var tmp map[string]json.RawMessage + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + + // set defaults for fields which might have been omitted + im.cache = registry.NewCache() + im.CustomAnalysis = newCustomAnalysis() + im.TypeField = defaultTypeField + im.DefaultType = defaultType + im.DefaultAnalyzer = defaultAnalyzer + im.DefaultDateTimeParser = defaultDateTimeParser + im.DefaultField = defaultField + im.DefaultMapping = NewDocumentMapping() + im.TypeMapping = make(map[string]*DocumentMapping) + im.StoreDynamic = StoreDynamic + im.IndexDynamic = IndexDynamic + im.DocValuesDynamic = DocValuesDynamic + + var invalidKeys []string + for k, v := range tmp { + switch k { + case "analysis": + err := util.UnmarshalJSON(v, &im.CustomAnalysis) + if err != nil { + return err + } + case "type_field": + err := util.UnmarshalJSON(v, &im.TypeField) + if err != nil { + return err + } + case "default_type": + err := util.UnmarshalJSON(v, &im.DefaultType) + if err != nil { + return err + } + case "default_analyzer": + err := util.UnmarshalJSON(v, &im.DefaultAnalyzer) + if err != nil { + return err + } + case "default_datetime_parser": + err := util.UnmarshalJSON(v, &im.DefaultDateTimeParser) + if err != nil { + return err + } + case "default_synonym_source": + err := util.UnmarshalJSON(v, &im.DefaultSynonymSource) + if err != nil { + return err + } + case "default_field": + err := util.UnmarshalJSON(v, &im.DefaultField) + if err != nil { + return err + } + case "default_mapping": + err := util.UnmarshalJSON(v, &im.DefaultMapping) + if err != nil { + return err + } + case "types": + err := util.UnmarshalJSON(v, &im.TypeMapping) + if err != nil { + return err + } + case "store_dynamic": + err := util.UnmarshalJSON(v, &im.StoreDynamic) + if err != nil { + return err + } + case "index_dynamic": + err := util.UnmarshalJSON(v, &im.IndexDynamic) + if err != nil { + return err + } + case "docvalues_dynamic": + err := util.UnmarshalJSON(v, &im.DocValuesDynamic) + if err != nil { + return err + } + case "scoring_model": + err := util.UnmarshalJSON(v, &im.ScoringModel) + if err != nil { + return err + } + + default: + invalidKeys = append(invalidKeys, k) + } + } + + if MappingJSONStrict && len(invalidKeys) > 0 { + return fmt.Errorf("index mapping contains invalid keys: %v", invalidKeys) + } + + err = im.CustomAnalysis.registerAll(im) + if err != nil { + return err + } + + return nil +} + +func (im *IndexMappingImpl) determineType(data interface{}) string { + // first see if the object implements bleveClassifier + bleveClassifier, ok := data.(bleveClassifier) + if ok { + return bleveClassifier.BleveType() + } + // next see if the object implements Classifier + classifier, ok := data.(Classifier) + if ok { + return classifier.Type() + } + + // now see if we can find a type using the mapping + typ, ok := mustString(lookupPropertyPath(data, im.TypeField)) + if ok { + return typ + } + + return im.DefaultType +} + +func (im *IndexMappingImpl) MapDocument(doc *document.Document, data interface{}) error { + docType := im.determineType(data) + docMapping := im.mappingForType(docType) + if docMapping.Enabled { + walkContext := im.newWalkContext(doc, docMapping) + docMapping.walkDocument(data, []string{}, []uint64{}, walkContext) + + // see if the _all field was disabled + allMapping, _ := docMapping.documentMappingForPath("_all") + if allMapping == nil || allMapping.Enabled { + field := document.NewCompositeFieldWithIndexingOptions("_all", true, []string{}, walkContext.excludedFromAll, index.IndexField|index.IncludeTermVectors) + doc.AddField(field) + } + doc.SetIndexed() + } + + return nil +} + +func (im *IndexMappingImpl) MapSynonymDocument(doc *document.Document, collection string, input []string, synonyms []string) error { + // determine all the synonym sources with the given collection + // and create a synonym field for each + err := im.SynonymSourceVisitor(func(name string, item analysis.SynonymSource) error { + if item.Collection() == collection { + // create a new field with the name of the synonym source + analyzer := im.AnalyzerNamed(item.Analyzer()) + if analyzer == nil { + return fmt.Errorf("unknown analyzer named: %s", item.Analyzer()) + } + field := document.NewSynonymField(name, analyzer, input, synonyms) + doc.AddField(field) + } + return nil + }) + return err +} + +type walkContext struct { + doc *document.Document + im *IndexMappingImpl + dm *DocumentMapping + excludedFromAll []string +} + +func (im *IndexMappingImpl) newWalkContext(doc *document.Document, dm *DocumentMapping) *walkContext { + return &walkContext{ + doc: doc, + im: im, + dm: dm, + excludedFromAll: []string{"_id"}, + } +} + +// AnalyzerNameForPath attempts to find the best analyzer to use with only a +// field name will walk all the document types, look for field mappings at the +// provided path, if one exists and it has an explicit analyzer that is +// returned. +func (im *IndexMappingImpl) AnalyzerNameForPath(path string) string { + // first we look for explicit mapping on the field + for _, docMapping := range im.TypeMapping { + analyzerName := docMapping.analyzerNameForPath(path) + if analyzerName != "" { + return analyzerName + } + } + + // now try the default mapping + pathMapping, _ := im.DefaultMapping.documentMappingForPath(path) + if pathMapping != nil { + if len(pathMapping.Fields) > 0 { + if pathMapping.Fields[0].Analyzer != "" { + return pathMapping.Fields[0].Analyzer + } + } + } + + // next we will try default analyzers for the path + pathDecoded := decodePath(path) + for _, docMapping := range im.TypeMapping { + if docMapping.Enabled { + rv := docMapping.defaultAnalyzerName(pathDecoded) + if rv != "" { + return rv + } + } + } + // now the default analyzer for the default mapping + if im.DefaultMapping.Enabled { + rv := im.DefaultMapping.defaultAnalyzerName(pathDecoded) + if rv != "" { + return rv + } + } + + return im.DefaultAnalyzer +} + +func (im *IndexMappingImpl) AnalyzerNamed(name string) analysis.Analyzer { + analyzer, err := im.cache.AnalyzerNamed(name) + if err != nil { + logger.Printf("error using analyzer named: %s", name) + return nil + } + return analyzer +} + +func (im *IndexMappingImpl) DateTimeParserNamed(name string) analysis.DateTimeParser { + if name == "" { + name = im.DefaultDateTimeParser + } + dateTimeParser, err := im.cache.DateTimeParserNamed(name) + if err != nil { + logger.Printf("error using datetime parser named: %s", name) + return nil + } + return dateTimeParser +} + +func (im *IndexMappingImpl) AnalyzeText(analyzerName string, text []byte) (analysis.TokenStream, error) { + analyzer, err := im.cache.AnalyzerNamed(analyzerName) + if err != nil { + return nil, err + } + return analyzer.Analyze(text), nil +} + +// FieldAnalyzer returns the name of the analyzer used on a field. +func (im *IndexMappingImpl) FieldAnalyzer(field string) string { + return im.AnalyzerNameForPath(field) +} + +// FieldMappingForPath returns the mapping for a specific field 'path'. +func (im *IndexMappingImpl) FieldMappingForPath(path string) FieldMapping { + if im.TypeMapping != nil { + for _, v := range im.TypeMapping { + fm := v.fieldDescribedByPath(path) + if fm != nil { + return *fm + } + } + } + + fm := im.DefaultMapping.fieldDescribedByPath(path) + if fm != nil { + return *fm + } + + return FieldMapping{} +} + +// wrapper to satisfy new interface + +func (im *IndexMappingImpl) DefaultSearchField() string { + return im.DefaultField +} + +func (im *IndexMappingImpl) SynonymSourceNamed(name string) analysis.SynonymSource { + syn, err := im.cache.SynonymSourceNamed(name) + if err != nil { + logger.Printf("error using synonym source named: %s", name) + return nil + } + return syn +} + +func (im *IndexMappingImpl) SynonymSourceForPath(path string) string { + // first we look for explicit mapping on the field + for _, docMapping := range im.TypeMapping { + synonymSource := docMapping.synonymSourceForPath(path) + if synonymSource != "" { + return synonymSource + } + } + + // now try the default mapping + pathMapping, _ := im.DefaultMapping.documentMappingForPath(path) + if pathMapping != nil { + if len(pathMapping.Fields) > 0 { + if pathMapping.Fields[0].SynonymSource != "" { + return pathMapping.Fields[0].SynonymSource + } + } + } + + // next we will try default synonym sources for the path + pathDecoded := decodePath(path) + for _, docMapping := range im.TypeMapping { + if docMapping.Enabled { + rv := docMapping.defaultSynonymSource(pathDecoded) + if rv != "" { + return rv + } + } + } + // now the default analyzer for the default mapping + if im.DefaultMapping.Enabled { + rv := im.DefaultMapping.defaultSynonymSource(pathDecoded) + if rv != "" { + return rv + } + } + + return im.DefaultSynonymSource +} + +// SynonymCount() returns the number of synonym sources defined in the mapping +func (im *IndexMappingImpl) SynonymCount() int { + return len(im.CustomAnalysis.SynonymSources) +} + +// SynonymSourceVisitor() allows a visitor to iterate over all synonym sources +func (im *IndexMappingImpl) SynonymSourceVisitor(visitor analysis.SynonymSourceVisitor) error { + err := im.cache.SynonymSources.VisitSynonymSources(visitor) + if err != nil { + return err + } + return nil +} diff --git a/mapping/mapping.go b/mapping/mapping.go new file mode 100644 index 0000000..a6c1591 --- /dev/null +++ b/mapping/mapping.go @@ -0,0 +1,76 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapping + +import ( + "io" + "log" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/document" +) + +// A Classifier is an interface describing any object which knows how to +// identify its own type. Alternatively, if a struct already has a Type +// field or method in conflict, one can use BleveType instead. +type Classifier interface { + Type() string +} + +// A bleveClassifier is an interface describing any object which knows how +// to identify its own type. This is introduced as an alternative to the +// Classifier interface which often has naming conflicts with existing +// structures. +type bleveClassifier interface { + BleveType() string +} + +var logger = log.New(io.Discard, "bleve mapping ", log.LstdFlags) + +// SetLog sets the logger used for logging +// by default log messages are sent to io.Discard +func SetLog(l *log.Logger) { + logger = l +} + +type IndexMapping interface { + MapDocument(doc *document.Document, data interface{}) error + Validate() error + + DateTimeParserNamed(name string) analysis.DateTimeParser + + DefaultSearchField() string + + AnalyzerNameForPath(path string) string + AnalyzerNamed(name string) analysis.Analyzer + + FieldMappingForPath(path string) FieldMapping +} + +// A SynonymMapping extends the IndexMapping interface to provide +// additional methods for working with synonyms. +type SynonymMapping interface { + IndexMapping + + MapSynonymDocument(doc *document.Document, collection string, input []string, synonyms []string) error + + SynonymSourceForPath(path string) string + + SynonymSourceNamed(name string) analysis.SynonymSource + + SynonymCount() int + + SynonymSourceVisitor(visitor analysis.SynonymSourceVisitor) error +} diff --git a/mapping/mapping_no_vectors.go b/mapping/mapping_no_vectors.go new file mode 100644 index 0000000..90cb1e2 --- /dev/null +++ b/mapping/mapping_no_vectors.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !vectors +// +build !vectors + +package mapping + +func NewVectorFieldMapping() *FieldMapping { + return nil +} + +func NewVectorBase64FieldMapping() *FieldMapping { + return nil +} + +func (fm *FieldMapping) processVector(propertyMightBeVector interface{}, + pathString string, path []string, indexes []uint64, context *walkContext) bool { + return false +} + +func (fm *FieldMapping) processVectorBase64(propertyMightBeVector interface{}, + pathString string, path []string, indexes []uint64, context *walkContext) { + +} + +// ----------------------------------------------------------------------------- +// document validation functions + +func validateFieldMapping(field *FieldMapping, parentName string, + fieldAliasCtx map[string]*FieldMapping) error { + return validateFieldType(field) +} diff --git a/mapping/mapping_test.go b/mapping/mapping_test.go new file mode 100644 index 0000000..e0151af --- /dev/null +++ b/mapping/mapping_test.go @@ -0,0 +1,1260 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapping + +import ( + "encoding/json" + "fmt" + index "github.com/blevesearch/bleve_index_api" + "reflect" + "strconv" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis/tokenizer/exception" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/regexp" + "github.com/blevesearch/bleve/v2/document" +) + +var mappingSource = []byte(`{ + "types": { + "beer": { + "properties": { + "name": { + "fields": [ + { + "name": "name", + "type": "text", + "analyzer": "standard", + "store": true, + "index": true, + "include_term_vectors": true, + "include_in_all": true, + "docvalues": true + } + ] + } + } + }, + "brewery": { + } + }, + "type_field": "_type", + "default_type": "_default" +}`) + +func buildMapping() IndexMapping { + nameFieldMapping := NewTextFieldMapping() + nameFieldMapping.Name = "name" + nameFieldMapping.Analyzer = "standard" + + beerMapping := NewDocumentMapping() + beerMapping.AddFieldMappingsAt("name", nameFieldMapping) + + breweryMapping := NewDocumentMapping() + + mapping := NewIndexMapping() + mapping.AddDocumentMapping("beer", beerMapping) + mapping.AddDocumentMapping("brewery", breweryMapping) + + return mapping +} + +func TestUnmarshalMappingJSON(t *testing.T) { + mapping := buildMapping() + + var indexMapping IndexMappingImpl + err := json.Unmarshal(mappingSource, &indexMapping) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(&indexMapping, mapping) { + t.Errorf("expected %#v,\n got %#v", mapping, &indexMapping) + } +} + +func TestMappingStructWithJSONTags(t *testing.T) { + + mapping := buildMapping() + + x := struct { + NoJSONTag string + Name string `json:"name"` + }{ + Name: "marty", + } + + doc := document.NewDocument("1") + err := mapping.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + foundJSONName := false + foundNoJSONName := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "name" { + foundJSONName = true + } + if f.Name() == "NoJSONTag" { + foundNoJSONName = true + } + count++ + } + if !foundJSONName { + t.Errorf("expected to find field named 'name'") + } + if !foundNoJSONName { + t.Errorf("expected to find field named 'NoJSONTag'") + } + if count != 2 { + t.Errorf("expected to find 2 find, found %d", count) + } +} + +func TestMappingStructWithJSONTagsOneDisabled(t *testing.T) { + + mapping := buildMapping() + + x := struct { + Name string `json:"name"` + Title string `json:"-"` + NoJSONTag string + }{ + Name: "marty", + } + + doc := document.NewDocument("1") + err := mapping.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + foundJSONName := false + foundNoJSONName := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "name" { + foundJSONName = true + } + if f.Name() == "NoJSONTag" { + foundNoJSONName = true + } + count++ + } + if !foundJSONName { + t.Errorf("expected to find field named 'name'") + } + if !foundNoJSONName { + t.Errorf("expected to find field named 'NoJSONTag'") + } + if count != 2 { + t.Errorf("expected to find 2 find, found %d", count) + } +} + +func TestMappingStructWithAlternateTags(t *testing.T) { + + mapping := buildMapping() + mapping.(*IndexMappingImpl).DefaultMapping.StructTagKey = "bleve" + + x := struct { + NoBLEVETag string + Name string `bleve:"name"` + }{ + Name: "marty", + } + + doc := document.NewDocument("1") + err := mapping.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + foundBLEVEName := false + foundNoBLEVEName := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "name" { + foundBLEVEName = true + } + if f.Name() == "NoBLEVETag" { + foundNoBLEVEName = true + } + count++ + } + if !foundBLEVEName { + t.Errorf("expected to find field named 'name'") + } + if !foundNoBLEVEName { + t.Errorf("expected to find field named 'NoBLEVETag'") + } + if count != 2 { + t.Errorf("expected to find 2 find, found %d", count) + } +} + +func TestMappingStructWithAlternateTagsTwoDisabled(t *testing.T) { + + mapping := buildMapping() + mapping.(*IndexMappingImpl).DefaultMapping.StructTagKey = "bleve" + + x := struct { + Name string `json:"-" bleve:"name"` + Title string `json:"-" bleve:"-"` + NoBLEVETag string `json:"-"` + Extra string `json:"extra" bleve:"-"` + }{ + Name: "marty", + } + + doc := document.NewDocument("1") + err := mapping.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + foundBLEVEName := false + foundNoBLEVEName := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "name" { + foundBLEVEName = true + } + if f.Name() == "NoBLEVETag" { + foundNoBLEVEName = true + } + count++ + } + if !foundBLEVEName { + t.Errorf("expected to find field named 'name'") + } + if !foundNoBLEVEName { + t.Errorf("expected to find field named 'NoBLEVETag'") + } + if count != 2 { + t.Errorf("expected to find 2 find, found %d", count) + } +} + +func TestMappingStructWithPointerToString(t *testing.T) { + + mapping := buildMapping() + + name := "marty" + + x := struct { + Name *string + }{ + Name: &name, + } + + doc := document.NewDocument("1") + err := mapping.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + found := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "Name" { + found = true + } + count++ + } + if !found { + t.Errorf("expected to find field named 'Name'") + } + if count != 1 { + t.Errorf("expected to find 1 find, found %d", count) + } +} + +func TestMappingJSONWithNull(t *testing.T) { + + mapping := NewIndexMapping() + + jsonbytes := []byte(`{"name":"marty", "age": null}`) + var jsondoc interface{} + err := json.Unmarshal(jsonbytes, &jsondoc) + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("1") + err = mapping.MapDocument(doc, jsondoc) + if err != nil { + t.Fatal(err) + } + found := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "name" { + found = true + } + count++ + } + if !found { + t.Errorf("expected to find field named 'name'") + } + if count != 1 { + t.Errorf("expected to find 1 find, found %d", count) + } +} + +func TestMappingForPath(t *testing.T) { + + enFieldMapping := NewTextFieldMapping() + enFieldMapping.Analyzer = "en" + + docMappingA := NewDocumentMapping() + docMappingA.AddFieldMappingsAt("name", enFieldMapping) + + customMapping := NewTextFieldMapping() + customMapping.Analyzer = "xyz" + customMapping.Name = "nameCustom" + + subDocMappingB := NewDocumentMapping() + customFieldX := NewTextFieldMapping() + customFieldX.Analyzer = "analyzerx" + subDocMappingB.AddFieldMappingsAt("desc", customFieldX) + + docMappingA.AddFieldMappingsAt("author", enFieldMapping, customMapping) + docMappingA.AddSubDocumentMapping("child", subDocMappingB) + + mapping := NewIndexMapping() + mapping.AddDocumentMapping("a", docMappingA) + + analyzerName := mapping.AnalyzerNameForPath("name") + if analyzerName != enFieldMapping.Analyzer { + t.Errorf("expected '%s' got '%s'", enFieldMapping.Analyzer, analyzerName) + } + + analyzerName = mapping.AnalyzerNameForPath("nameCustom") + if analyzerName != customMapping.Analyzer { + t.Errorf("expected '%s' got '%s'", customMapping.Analyzer, analyzerName) + } + + analyzerName = mapping.AnalyzerNameForPath("child.desc") + if analyzerName != customFieldX.Analyzer { + t.Errorf("expected '%s' got '%s'", customFieldX.Analyzer, analyzerName) + } + +} + +func TestMappingWithTokenizerDeps(t *testing.T) { + + tokNoDeps := map[string]interface{}{ + "type": regexp.Name, + "regexp": "", + } + + tokDepsL1 := map[string]interface{}{ + "type": exception.Name, + "tokenizer": "a", + "exceptions": []string{".*"}, + } + + // this tests a 1-level dependency + // it is run 100 times to increase the + // likelihood that it fails along time way + // (depends on key order iteration in map) + for i := 0; i < 100; i++ { + + m := NewIndexMapping() + ca := customAnalysis{ + Tokenizers: map[string]map[string]interface{}{ + "a": tokNoDeps, + "b": tokDepsL1, + }, + } + err := ca.registerAll(m) + if err != nil { + t.Fatal(err) + } + } + + tokDepsL2 := map[string]interface{}{ + "type": "exception", + "tokenizer": "b", + "exceptions": []string{".*"}, + } + + // now test a second-level dependency + for i := 0; i < 100; i++ { + + m := NewIndexMapping() + ca := customAnalysis{ + Tokenizers: map[string]map[string]interface{}{ + "a": tokNoDeps, + "b": tokDepsL1, + "c": tokDepsL2, + }, + } + err := ca.registerAll(m) + if err != nil { + t.Fatal(err) + } + } + + tokUnsatisfied := map[string]interface{}{ + "type": "exception", + "tokenizer": "e", + } + + // now make sure an unsatisfied dep still + // results in an error + m := NewIndexMapping() + ca := customAnalysis{ + Tokenizers: map[string]map[string]interface{}{ + "a": tokNoDeps, + "b": tokDepsL1, + "c": tokDepsL2, + "d": tokUnsatisfied, + }, + } + err := ca.registerAll(m) + if err == nil { + t.Fatal(err) + } +} + +func TestEnablingDisablingStoringDynamicFields(t *testing.T) { + + // first verify that with system defaults, dynamic field is stored + data := map[string]interface{}{ + "name": "bleve", + } + doc := document.NewDocument("x") + mapping := NewIndexMapping() + err := mapping.MapDocument(doc, data) + if err != nil { + t.Fatal(err) + } + for _, field := range doc.Fields { + if field.Name() == "name" && !field.Options().IsStored() { + t.Errorf("expected field 'name' to be stored, isn't") + } + } + + // now change system level defaults, verify dynamic field is not stored + StoreDynamic = false + defer func() { + StoreDynamic = true + }() + + mapping = NewIndexMapping() + doc = document.NewDocument("y") + err = mapping.MapDocument(doc, data) + if err != nil { + t.Fatal(err) + } + for _, field := range doc.Fields { + if field.Name() == "name" && field.Options().IsStored() { + t.Errorf("expected field 'name' to be not stored, is") + } + } + + // now override the system level defaults inside the index mapping + mapping = NewIndexMapping() + mapping.StoreDynamic = true + doc = document.NewDocument("y") + err = mapping.MapDocument(doc, data) + if err != nil { + t.Fatal(err) + } + for _, field := range doc.Fields { + if field.Name() == "name" && !field.Options().IsStored() { + t.Errorf("expected field 'name' to be stored, isn't") + } + } +} + +func TestMappingBool(t *testing.T) { + boolMapping := NewBooleanFieldMapping() + docMapping := NewDocumentMapping() + docMapping.AddFieldMappingsAt("prop", boolMapping) + mapping := NewIndexMapping() + mapping.AddDocumentMapping("doc", docMapping) + + pprop := false + x := struct { + Prop bool `json:"prop"` + PProp *bool `json:"pprop"` + }{ + Prop: true, + PProp: &pprop, + } + + doc := document.NewDocument("1") + err := mapping.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + foundProp := false + foundPProp := false + count := 0 + for _, f := range doc.Fields { + if f.Name() == "prop" { + foundProp = true + } + if f.Name() == "pprop" { + foundPProp = true + } + count++ + } + if !foundProp { + t.Errorf("expected to find bool field named 'prop'") + } + if !foundPProp { + t.Errorf("expected to find pointer to bool field named 'pprop'") + } + if count != 2 { + t.Errorf("expected to find 2 fields, found %d", count) + } +} + +func TestDisableDefaultMapping(t *testing.T) { + indexMapping := NewIndexMapping() + indexMapping.DefaultMapping.Enabled = false + + data := map[string]string{ + "name": "bleve", + } + + doc := document.NewDocument("x") + err := indexMapping.MapDocument(doc, data) + if err != nil { + t.Error(err) + } + + if len(doc.Fields) > 0 { + t.Errorf("expected no fields, got %d", len(doc.Fields)) + } +} + +func TestInvalidFieldMappingStrict(t *testing.T) { + mappingBytes := []byte(`{"includeInAll":true,"name":"a parsed name"}`) + + // first unmarhsal it without strict + var fm FieldMapping + err := json.Unmarshal(mappingBytes, &fm) + if err != nil { + t.Fatal(err) + } + + if fm.Name != "a parsed name" { + t.Fatalf("expect to find field mapping name 'a parsed name', got '%s'", fm.Name) + } + + // reset + fm.Name = "" + + // now enable strict + MappingJSONStrict = true + defer func() { + MappingJSONStrict = false + }() + + expectedInvalidKeys := []string{"includeInAll"} + expectedErr := fmt.Errorf("field mapping contains invalid keys: %v", expectedInvalidKeys) + err = json.Unmarshal(mappingBytes, &fm) + if err.Error() != expectedErr.Error() { + t.Fatalf("expected err: %v, got err: %v", expectedErr, err) + } + + if fm.Name != "a parsed name" { + t.Fatalf("expect to find field mapping name 'a parsed name', got '%s'", fm.Name) + } + +} + +func TestInvalidDocumentMappingStrict(t *testing.T) { + mappingBytes := []byte(`{"defaultAnalyzer":true,"enabled":false}`) + + // first unmarhsal it without strict + var dm DocumentMapping + err := json.Unmarshal(mappingBytes, &dm) + if err != nil { + t.Fatal(err) + } + + if dm.Enabled != false { + t.Fatalf("expect to find document mapping enabled false, got '%t'", dm.Enabled) + } + + // reset + dm.Enabled = true + + // now enable strict + MappingJSONStrict = true + defer func() { + MappingJSONStrict = false + }() + + expectedInvalidKeys := []string{"defaultAnalyzer"} + expectedErr := fmt.Errorf("document mapping contains invalid keys: %v", expectedInvalidKeys) + err = json.Unmarshal(mappingBytes, &dm) + if err.Error() != expectedErr.Error() { + t.Fatalf("expected err: %v, got err: %v", expectedErr, err) + } + + if dm.Enabled != false { + t.Fatalf("expect to find document mapping enabled false, got '%t'", dm.Enabled) + } +} + +func TestInvalidIndexMappingStrict(t *testing.T) { + mappingBytes := []byte(`{"typeField":"type","default_field":"all"}`) + + // first unmarhsal it without strict + var im IndexMappingImpl + err := json.Unmarshal(mappingBytes, &im) + if err != nil { + t.Fatal(err) + } + + if im.DefaultField != "all" { + t.Fatalf("expect to find index mapping default field 'all', got '%s'", im.DefaultField) + } + + // reset + im.DefaultField = "_all" + + // now enable strict + MappingJSONStrict = true + defer func() { + MappingJSONStrict = false + }() + + expectedInvalidKeys := []string{"typeField"} + expectedErr := fmt.Errorf("index mapping contains invalid keys: %v", expectedInvalidKeys) + err = json.Unmarshal(mappingBytes, &im) + if err.Error() != expectedErr.Error() { + t.Fatalf("expected err: %v, got err: %v", expectedErr, err) + } + + if im.DefaultField != "all" { + t.Fatalf("expect to find index mapping default field 'all', got '%s'", im.DefaultField) + } +} + +func TestMappingBug353(t *testing.T) { + dataBytes := `{ + "Reviews": [ + { + "ReviewID": "RX16692001", + "Content": "Usually stay near the airport..." + } + ], + "Other": { + "Inside": "text" + }, + "Name": "The Inn at Baltimore White Marsh" +}` + + var data map[string]interface{} + err := json.Unmarshal([]byte(dataBytes), &data) + if err != nil { + t.Fatal(err) + } + + reviewContentFieldMapping := NewTextFieldMapping() + reviewContentFieldMapping.Analyzer = "crazy" + + reviewsMapping := NewDocumentMapping() + reviewsMapping.Dynamic = false + reviewsMapping.AddFieldMappingsAt("Content", reviewContentFieldMapping) + otherMapping := NewDocumentMapping() + otherMapping.Dynamic = false + mapping := NewIndexMapping() + mapping.DefaultMapping.AddSubDocumentMapping("Reviews", reviewsMapping) + mapping.DefaultMapping.AddSubDocumentMapping("Other", otherMapping) + + doc := document.NewDocument("x") + err = mapping.MapDocument(doc, data) + if err != nil { + t.Fatal(err) + } + + // expect doc has only 2 fields + if len(doc.Fields) != 2 { + t.Errorf("expected doc with 2 fields, got: %d", len(doc.Fields)) + for _, f := range doc.Fields { + t.Logf("field named: %s", f.Name()) + } + } +} + +func TestAnonymousStructFields(t *testing.T) { + + type Contact0 string + + type Contact1 struct { + Name string + } + + type Contact2 interface{} + + type Contact3 interface{} + + type Thing struct { + Contact0 + Contact1 + Contact2 + Contact3 + } + + x := Thing{ + Contact0: "hello", + Contact1: Contact1{ + Name: "marty", + }, + Contact2: Contact1{ + Name: "will", + }, + Contact3: "steve", + } + + doc := document.NewDocument("1") + m := NewIndexMapping() + err := m.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + + if len(doc.Fields) != 4 { + t.Fatalf("expected 4 fields, got %d", len(doc.Fields)) + } + if doc.Fields[0].Name() != "Contact0" { + t.Errorf("expected field named 'Contact0', got '%s'", doc.Fields[0].Name()) + } + if doc.Fields[1].Name() != "Name" { + t.Errorf("expected field named 'Name', got '%s'", doc.Fields[1].Name()) + } + if doc.Fields[2].Name() != "Contact2.Name" { + t.Errorf("expected field named 'Contact2.Name', got '%s'", doc.Fields[2].Name()) + } + if doc.Fields[3].Name() != "Contact3" { + t.Errorf("expected field named 'Contact3', got '%s'", doc.Fields[3].Name()) + } + + type AnotherThing struct { + Contact0 `json:"Alternate0"` + Contact1 `json:"Alternate1"` + Contact2 `json:"Alternate2"` + Contact3 `json:"Alternate3"` + } + + y := AnotherThing{ + Contact0: "hello", + Contact1: Contact1{ + Name: "marty", + }, + Contact2: Contact1{ + Name: "will", + }, + Contact3: "steve", + } + + doc2 := document.NewDocument("2") + err = m.MapDocument(doc2, y) + if err != nil { + t.Fatal(err) + } + + if len(doc2.Fields) != 4 { + t.Fatalf("expected 4 fields, got %d", len(doc2.Fields)) + } + if doc2.Fields[0].Name() != "Alternate0" { + t.Errorf("expected field named 'Alternate0', got '%s'", doc2.Fields[0].Name()) + } + if doc2.Fields[1].Name() != "Alternate1.Name" { + t.Errorf("expected field named 'Name', got '%s'", doc2.Fields[1].Name()) + } + if doc2.Fields[2].Name() != "Alternate2.Name" { + t.Errorf("expected field named 'Alternate2.Name', got '%s'", doc2.Fields[2].Name()) + } + if doc2.Fields[3].Name() != "Alternate3" { + t.Errorf("expected field named 'Alternate3', got '%s'", doc2.Fields[3].Name()) + } +} + +func TestAnonymousStructFieldWithJSONStructTagEmptString(t *testing.T) { + type InterfaceThing interface{} + type Thing struct { + InterfaceThing `json:""` + } + + x := Thing{ + InterfaceThing: map[string]interface{}{ + "key": "value", + }, + } + + doc := document.NewDocument("1") + m := NewIndexMapping() + err := m.MapDocument(doc, x) + if err != nil { + t.Fatal(err) + } + + if len(doc.Fields) != 1 { + t.Fatalf("expected 1 field, got %d", len(doc.Fields)) + } + if doc.Fields[0].Name() != "key" { + t.Errorf("expected field named 'key', got '%s'", doc.Fields[0].Name()) + } +} + +func TestMappingPrimitives(t *testing.T) { + + tests := []struct { + data interface{} + }{ + {data: "marty"}, + {data: int(1)}, + {data: int8(2)}, + {data: int16(3)}, + {data: int32(4)}, + {data: int64(5)}, + {data: uint(6)}, + {data: uint8(7)}, + {data: uint16(8)}, + {data: uint32(9)}, + {data: uint64(10)}, + {data: float32(11.0)}, + {data: float64(12.0)}, + {data: false}, + } + + m := NewIndexMapping() + for _, test := range tests { + doc := document.NewDocument("x") + err := m.MapDocument(doc, test.data) + if err != nil { + t.Fatal(err) + } + if len(doc.Fields) != 1 { + t.Errorf("expected 1 field, got %d for %v", len(doc.Fields), test.data) + } + } +} + +func TestMappingForGeo(t *testing.T) { + + type Location struct { + Lat float64 + Lon float64 + } + + nameFieldMapping := NewTextFieldMapping() + nameFieldMapping.Name = "name" + nameFieldMapping.Analyzer = "standard" + + locFieldMapping := NewGeoPointFieldMapping() + + thingMapping := NewDocumentMapping() + thingMapping.AddFieldMappingsAt("name", nameFieldMapping) + thingMapping.AddFieldMappingsAt("location", locFieldMapping) + + mapping := NewIndexMapping() + mapping.DefaultMapping = thingMapping + + geopoints := []interface{}{} + expect := [][]float64{} // to contain expected [lon,lat] for geopoints + + // geopoint as a struct + geopoints = append(geopoints, struct { + Name string `json:"name"` + Location *Location `json:"location"` + }{ + Name: "struct", + Location: &Location{ + Lon: -180, + Lat: -90, + }, + }) + expect = append(expect, []float64{-180, -90}) + + // geopoint as a map + geopoints = append(geopoints, struct { + Name string `json:"name"` + Location map[string]interface{} `json:"location"` + }{ + Name: "map", + Location: map[string]interface{}{ + "lon": -180, + "lat": -90, + }, + }) + expect = append(expect, []float64{-180, -90}) + + // geopoint as a slice, format: {lon, lat} + geopoints = append(geopoints, struct { + Name string `json:"name"` + Location []interface{} `json:"location"` + }{ + Name: "slice", + Location: []interface{}{ + -180, -90, + }, + }) + expect = append(expect, []float64{-180, -90}) + + // geopoint as a string, format: "lat,lon" + geopoints = append(geopoints, struct { + Name string `json:"name"` + Location []interface{} `json:"location"` + }{ + Name: "string", + Location: []interface{}{ + "-90,-180", + }, + }) + expect = append(expect, []float64{-180, -90}) + + // geopoint as a string, format: "lat , lon" with leading/trailing whitespaces + geopoints = append(geopoints, struct { + Name string `json:"name"` + Location []interface{} `json:"location"` + }{ + Name: "string", + Location: []interface{}{ + "-90 , -180", + }, + }) + expect = append(expect, []float64{-180, -90}) + + // geopoint as a string - geohash + geopoints = append(geopoints, struct { + Name string `json:"name"` + Location []interface{} `json:"location"` + }{ + Name: "string", + Location: []interface{}{ + "000000000000", + }, + }) + expect = append(expect, []float64{-180, -90}) + + // geopoint as a string - geohash + geopoints = append(geopoints, struct { + Name string `json:"name"` + Location []interface{} `json:"location"` + }{ + Name: "string", + Location: []interface{}{ + "drm3btev3e86", + }, + }) + expect = append(expect, []float64{-71.34, 41.12}) + + for i, geopoint := range geopoints { + doc := document.NewDocument(fmt.Sprint(i)) + err := mapping.MapDocument(doc, geopoint) + if err != nil { + t.Fatal(err) + } + + var foundGeo bool + for _, f := range doc.Fields { + if f.Name() == "location" { + foundGeo = true + geoF, ok := f.(index.GeoPointField) + if !ok { + t.Errorf("expected a geopoint field!") + } + lon, err := geoF.Lon() + if err != nil { + t.Errorf("error in fetching lon, err: %v", err) + } + lat, err := geoF.Lat() + if err != nil { + t.Errorf("error in fetching lat, err: %v", err) + } + // round obtained lon, lat to 2 decimal places + roundLon, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", lon), 64) + roundLat, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", lat), 64) + if roundLon != expect[i][0] || roundLat != expect[i][1] { + t.Errorf("expected geo point: {%v, %v}, got {%v, %v}", + expect[i][0], expect[i][1], lon, lat) + } + } + } + + if !foundGeo { + t.Errorf("expected to find geo point, did not") + } + } +} + +type textMarshalable struct { + body string + Extra string +} + +func (t *textMarshalable) MarshalText() ([]byte, error) { + return []byte(t.body), nil +} + +func TestMappingForTextMarshaler(t *testing.T) { + tm := struct { + Marshalable *textMarshalable + }{ + Marshalable: &textMarshalable{ + body: "text", + Extra: "stuff", + }, + } + + // first verify that when using a mapping that doesn't explicitly + // map the stuct field as text, then we traverse inside the struct + // and do our best + m := NewIndexMapping() + doc := document.NewDocument("x") + err := m.MapDocument(doc, tm) + if err != nil { + t.Fatal(err) + } + + if len(doc.Fields) != 1 { + t.Fatalf("expected 1 field, got: %d", len(doc.Fields)) + } + if doc.Fields[0].Name() != "Marshalable.Extra" { + t.Errorf("expected field to be named 'Marshalable.Extra', got: '%s'", doc.Fields[0].Name()) + } + if string(doc.Fields[0].Value()) != tm.Marshalable.Extra { + t.Errorf("expected field value to be '%s', got: '%s'", tm.Marshalable.Extra, string(doc.Fields[0].Value())) + } + + // now verify that when a mapping explicitly + m = NewIndexMapping() + txt := NewTextFieldMapping() + m.DefaultMapping.AddFieldMappingsAt("Marshalable", txt) + doc = document.NewDocument("x") + err = m.MapDocument(doc, tm) + if err != nil { + t.Fatal(err) + } + + if len(doc.Fields) != 1 { + t.Fatalf("expected 1 field, got: %d", len(doc.Fields)) + + } + if doc.Fields[0].Name() != "Marshalable" { + t.Errorf("expected field to be named 'Marshalable', got: '%s'", doc.Fields[0].Name()) + } + want, err := tm.Marshalable.MarshalText() + if err != nil { + t.Fatal(err) + } + if string(doc.Fields[0].Value()) != string(want) { + t.Errorf("expected field value to be '%s', got: '%s'", string(want), string(doc.Fields[0].Value())) + } + +} + +func TestMappingForNilTextMarshaler(t *testing.T) { + tm := struct { + Marshalable *time.Time + }{ + Marshalable: nil, + } + + // now verify that when a mapping explicitly + m := NewIndexMapping() + txt := NewTextFieldMapping() + m.DefaultMapping.AddFieldMappingsAt("Marshalable", txt) + doc := document.NewDocument("x") + err := m.MapDocument(doc, tm) + if err != nil { + t.Fatal(err) + } + + if len(doc.Fields) != 0 { + t.Fatalf("expected 1 field, got: %d", len(doc.Fields)) + + } + +} + +func TestClosestDocDynamicMapping(t *testing.T) { + mapping := NewIndexMapping() + mapping.IndexDynamic = false + mapping.DefaultMapping = NewDocumentStaticMapping() + mapping.DefaultMapping.AddFieldMappingsAt("foo", NewTextFieldMapping()) + + doc := document.NewDocument("x") + err := mapping.MapDocument(doc, map[string]interface{}{ + "foo": "value", + "bar": map[string]string{ + "foo": "value2", + "baz": "value3", + }, + }) + if err != nil { + t.Fatal(err) + } + + if len(doc.Fields) != 1 { + t.Fatalf("expected 1 field, got: %d", len(doc.Fields)) + } +} + +func TestMappingPointerToTimeBug1152(t *testing.T) { + when, err := time.Parse(time.RFC3339, "2019-03-06T15:04:05Z") + if err != nil { + t.Fatal(err) + } + + thing := struct { + When *time.Time + }{ + When: &when, + } + + // this case tests when there WAS an explicit mapping, but it was NOT type text + // as this was the specific case that was problematic + m := NewIndexMapping() + dtf := NewDateTimeFieldMapping() + m.DefaultMapping.AddFieldMappingsAt("When", dtf) + doc := document.NewDocument("x") + err = m.MapDocument(doc, thing) + if err != nil { + t.Fatal(err) + } + + if len(doc.Fields) != 1 { + t.Fatalf("expected 1 field, got: %d", len(doc.Fields)) + } + if _, ok := doc.Fields[0].(index.DateTimeField); !ok { + t.Fatalf("expected field to be type *document.DateTimeField, got %T", doc.Fields[0]) + } +} + +func TestDefaultAnalyzerInheritance(t *testing.T) { + docMapping := NewDocumentMapping() + docMapping.DefaultAnalyzer = "xyz" + childMapping := NewTextFieldMapping() + docMapping.AddFieldMappingsAt("field", childMapping) + + if analyzer := docMapping.defaultAnalyzerName([]string{"field"}); analyzer != "xyz" { + t.Fatalf("Expected analyzer: xyz to be inherited by field, but got: '%v'", analyzer) + } +} + +func TestWrongAnalyzerSearchableAs(t *testing.T) { + fieldMapping := NewTextFieldMapping() + fieldMapping.Name = "geo.accuracy" + fieldMapping.Analyzer = "xyz" + + nestedMapping := NewDocumentMapping() + nestedMapping.AddFieldMappingsAt("accuracy", fieldMapping) + + docMapping := NewDocumentMapping() + docMapping.AddSubDocumentMapping("geo", nestedMapping) + + indexMapping := NewIndexMapping() + indexMapping.AddDocumentMapping("brewery", docMapping) + + analyzerName := indexMapping.AnalyzerNameForPath("geo.geo.accuracy") + if analyzerName != "xyz" { + t.Errorf("expected analyzer name `xyz`, got `%s`", analyzerName) + } +} + +func TestMappingArrayOfStringGeoPoints(t *testing.T) { + nameFieldMapping := NewTextFieldMapping() + nameFieldMapping.Name = "name" + nameFieldMapping.Analyzer = "standard" + + locFieldMapping := NewGeoPointFieldMapping() + + thingMapping := NewDocumentMapping() + thingMapping.AddFieldMappingsAt("points", locFieldMapping) + + mapping := NewIndexMapping() + mapping.DefaultMapping = thingMapping + + docs := []map[string]interface{}{ + { + // string: "lat,lon" + "points": []string{ + "1.0, 2.0", + "3.0, 4.0", + "5.0, 6.0", + }, + }, + { + // slice: {lon, lat} + "points": [][]float64{ + {2.0, 1.0}, + {4.0, 3.0}, + {6.0, 5.0}, + }, + }, + { + // struct: {"lon/lng": .., "lat": ..} + "points": []map[string]interface{}{ + {"lon": 2.0, "lat": 1.0}, + {"lng": 4.0, "lat": 3.0}, + {"lng": 6.0, "lat": 5.0}, + }, + }, + } + + for _, docSrc := range docs { + doc := document.NewDocument("x") + err := mapping.MapDocument(doc, docSrc) + if err != nil { + t.Fatal(err) + } + + // points here in lon, lat order + expectPoints := map[string][]float64{ + "first": {2.0, 1.0}, + "second": {4.0, 3.0}, + "third": {6.0, 5.0}, + } + + for _, f := range doc.Fields { + if f.Name() == "points" { + geoF, ok := f.(*document.GeoPointField) + if !ok { + t.Errorf("expected a geopoint field!") + } + lon, err := geoF.Lon() + if err != nil { + t.Errorf("error in fetching lon, err: %v", err) + } + lat, err := geoF.Lat() + if err != nil { + t.Errorf("error in fetching lat, err: %v", err) + } + // round obtained lon, lat to 2 decimal places + roundLon, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", lon), 64) + roundLat, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", lat), 64) + + for key, point := range expectPoints { + if roundLon == point[0] && roundLat == point[1] { + delete(expectPoints, key) + } + } + } + } + + if len(expectPoints) > 0 { + t.Errorf("some points not found: %v", expectPoints) + } + } +} diff --git a/mapping/mapping_vectors.go b/mapping/mapping_vectors.go new file mode 100644 index 0000000..20cbac6 --- /dev/null +++ b/mapping/mapping_vectors.go @@ -0,0 +1,272 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package mapping + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" + faiss "github.com/blevesearch/go-faiss" +) + +// Min and Max allowed dimensions for a vector field; +// p.s must be set/updated at process init() _only_ +var ( + MinVectorDims = 1 + MaxVectorDims = 4096 +) + +func NewVectorFieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "vector", + Store: false, + Index: true, + IncludeInAll: false, + DocValues: false, + SkipFreqNorm: true, + } +} + +func NewVectorBase64FieldMapping() *FieldMapping { + return &FieldMapping{ + Type: "vector_base64", + Store: false, + Index: true, + IncludeInAll: false, + DocValues: false, + SkipFreqNorm: true, + } +} + +// validate and process a flat vector +func processFlatVector(vecV reflect.Value, dims int) ([]float32, bool) { + if vecV.Len() != dims { + return nil, false + } + + rv := make([]float32, dims) + for i := 0; i < vecV.Len(); i++ { + item := vecV.Index(i) + if !item.CanInterface() { + return nil, false + } + itemI := item.Interface() + itemFloat, ok := util.ExtractNumericValFloat32(itemI) + if !ok { + return nil, false + } + rv[i] = itemFloat + } + + return rv, true +} + +// validate and process a vector +// max supported depth of nesting is 2 ([][]float32) +func processVector(vecI interface{}, dims int) ([]float32, bool) { + vecV := reflect.ValueOf(vecI) + if !vecV.IsValid() || vecV.Kind() != reflect.Slice || vecV.Len() == 0 { + return nil, false + } + + // Let's examine the first element (head) of the vector. + // If head is a slice, then vector is nested, otherwise flat. + head := vecV.Index(0) + if !head.CanInterface() { + return nil, false + } + headI := head.Interface() + headV := reflect.ValueOf(headI) + if !headV.IsValid() { + return nil, false + } + if headV.Kind() != reflect.Slice { // vector is flat + return processFlatVector(vecV, dims) + } + + // # process nested vector + + // pre-allocate memory for the flattened vector + // so that we can use copy() later + rv := make([]float32, dims*vecV.Len()) + + for i := 0; i < vecV.Len(); i++ { + subVec := vecV.Index(i) + if !subVec.CanInterface() { + return nil, false + } + subVecI := subVec.Interface() + subVecV := reflect.ValueOf(subVecI) + if !subVecV.IsValid() { + return nil, false + } + + if subVecV.Kind() != reflect.Slice { + return nil, false + } + + flatVector, ok := processFlatVector(subVecV, dims) + if !ok { + return nil, false + } + + copy(rv[i*dims:(i+1)*dims], flatVector) + } + + return rv, true +} + +func (fm *FieldMapping) processVector(propertyMightBeVector interface{}, + pathString string, path []string, indexes []uint64, context *walkContext) bool { + vector, ok := processVector(propertyMightBeVector, fm.Dims) + // Don't add field to document if vector is invalid + if !ok { + return false + } + // normalize raw vector if similarity is cosine + if fm.Similarity == index.CosineSimilarity { + vector = NormalizeVector(vector) + } + + fieldName := getFieldName(pathString, path, fm) + options := fm.Options() + field := document.NewVectorFieldWithIndexingOptions(fieldName, indexes, vector, + fm.Dims, fm.Similarity, fm.VectorIndexOptimizedFor, options) + context.doc.AddField(field) + + // "_all" composite field is not applicable for vector field + context.excludedFromAll = append(context.excludedFromAll, fieldName) + return true +} + +func (fm *FieldMapping) processVectorBase64(propertyMightBeVectorBase64 interface{}, + pathString string, path []string, indexes []uint64, context *walkContext) { + encodedString, ok := propertyMightBeVectorBase64.(string) + if !ok { + return + } + + decodedVector, err := document.DecodeVector(encodedString) + if err != nil || len(decodedVector) != fm.Dims { + return + } + // normalize raw vector if similarity is cosine + if fm.Similarity == index.CosineSimilarity { + decodedVector = NormalizeVector(decodedVector) + } + + fieldName := getFieldName(pathString, path, fm) + options := fm.Options() + field := document.NewVectorFieldWithIndexingOptions(fieldName, indexes, decodedVector, + fm.Dims, fm.Similarity, fm.VectorIndexOptimizedFor, options) + context.doc.AddField(field) + + // "_all" composite field is not applicable for vector_base64 field + context.excludedFromAll = append(context.excludedFromAll, fieldName) +} + +// ----------------------------------------------------------------------------- +// document validation functions + +func validateFieldMapping(field *FieldMapping, parentName string, + fieldAliasCtx map[string]*FieldMapping) error { + switch field.Type { + case "vector", "vector_base64": + return validateVectorFieldAlias(field, parentName, fieldAliasCtx) + default: // non-vector field + return validateFieldType(field) + } +} + +func validateVectorFieldAlias(field *FieldMapping, parentName string, + fieldAliasCtx map[string]*FieldMapping) error { + + if field.Name == "" { + field.Name = parentName + } + + if field.Similarity == "" { + field.Similarity = index.DefaultVectorSimilarityMetric + } + + if field.VectorIndexOptimizedFor == "" { + field.VectorIndexOptimizedFor = index.DefaultIndexOptimization + } + if _, exists := index.SupportedVectorIndexOptimizations[field.VectorIndexOptimizedFor]; !exists { + // if an unsupported config is provided, override to default + field.VectorIndexOptimizedFor = index.DefaultIndexOptimization + } + + // following fields are not applicable for vector + // thus, we set them to default values + field.IncludeInAll = false + field.IncludeTermVectors = false + field.Store = false + field.DocValues = false + field.SkipFreqNorm = true + + // # If alias is present, validate the field options as per the alias + // note: reading from a nil map is safe + if fieldAlias, ok := fieldAliasCtx[field.Name]; ok { + if field.Dims != fieldAlias.Dims { + return fmt.Errorf("field: '%s', invalid alias "+ + "(different dimensions %d and %d)", fieldAlias.Name, field.Dims, + fieldAlias.Dims) + } + + if field.Similarity != fieldAlias.Similarity { + return fmt.Errorf("field: '%s', invalid alias "+ + "(different similarity values %s and %s)", fieldAlias.Name, + field.Similarity, fieldAlias.Similarity) + } + + return nil + } + + // # Validate field options + + if field.Dims < MinVectorDims || field.Dims > MaxVectorDims { + return fmt.Errorf("field: '%s', invalid vector dimension: %d,"+ + " value should be in range (%d, %d)", field.Name, field.Dims, + MinVectorDims, MaxVectorDims) + } + + if _, ok := index.SupportedVectorSimilarityMetrics[field.Similarity]; !ok { + return fmt.Errorf("field: '%s', invalid similarity "+ + "metric: '%s', valid metrics are: %+v", field.Name, field.Similarity, + reflect.ValueOf(index.SupportedVectorSimilarityMetrics).MapKeys()) + } + + if fieldAliasCtx != nil { // writing to a nil map is unsafe + fieldAliasCtx[field.Name] = field + } + + return nil +} + +func NormalizeVector(vec []float32) []float32 { + // make a copy of the vector to avoid modifying the original + // vector in-place + vecCopy := make([]float32, len(vec)) + copy(vecCopy, vec) + // normalize the vector copy using in-place normalization provided by faiss + return faiss.NormalizeVector(vecCopy) +} diff --git a/mapping/mapping_vectors_test.go b/mapping/mapping_vectors_test.go new file mode 100644 index 0000000..b974237 --- /dev/null +++ b/mapping/mapping_vectors_test.go @@ -0,0 +1,334 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package mapping + +import ( + "reflect" + "testing" +) + +func TestVectorFieldAliasValidation(t *testing.T) { + tests := []struct { + // input + name string // name of the test + mappingStr string // index mapping json string + + // expected output + expValidity bool // validity of the mapping + errMsgs []string // error message, given expValidity is false + }{ + { + name: "test1", + mappingStr: ` + { + "default_mapping": { + "properties": { + "cityVec": { + "fields": [ + { + "type": "vector", + "dims": 3 + }, + { + "name": "cityVec", + "type": "vector", + "dims": 4 + } + ] + } + } + } + }`, + expValidity: false, + errMsgs: []string{`field: 'cityVec', invalid alias (different dimensions 4 and 3)`}, + }, + { + name: "test2", + mappingStr: ` + { + "default_mapping": { + "properties": { + "cityVec": { + "fields": [ + { + "type": "vector", + "dims": 3, + "similarity": "l2_norm" + }, + { + "name": "cityVec", + "type": "vector", + "dims": 3, + "similarity": "dot_product" + } + ] + } + } + } + }`, + expValidity: false, + errMsgs: []string{`field: 'cityVec', invalid alias (different similarity values dot_product and l2_norm)`}, + }, + { + name: "test3", + mappingStr: ` + { + "default_mapping": { + "properties": { + "cityVec": { + "fields": [ + { + "type": "vector", + "dims": 3 + }, + { + "name": "cityVec", + "type": "vector", + "dims": 3 + } + ] + } + } + } + }`, + expValidity: true, + errMsgs: []string{}, + }, + { + name: "test4", + mappingStr: ` + { + "default_mapping": { + "properties": { + "cityVec": { + "fields": [ + { + "name": "vecData", + "type": "vector", + "dims": 4 + } + ] + }, + "countryVec": { + "fields": [ + { + "name": "vecData", + "type": "vector", + "dims": 3 + } + ] + } + } + } + }`, + expValidity: false, + errMsgs: []string{`field: 'vecData', invalid alias (different dimensions 3 and 4)`, `field: 'vecData', invalid alias (different dimensions 4 and 3)`}, + }, + { + name: "test5", + mappingStr: ` + { + "default_mapping": { + "properties": { + "cityVec": { + "fields": [ + { + "name": "vecData", + "type": "vector", + "dims": 3 + } + ] + } + } + }, + "types": { + "type1": { + "properties": { + "cityVec": { + "fields": [ + { + "name": "vecData", + "type": "vector", + "dims": 4 + } + ] + } + } + } + } + }`, + expValidity: false, + errMsgs: []string{`field: 'vecData', invalid alias (different dimensions 4 and 3)`}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + im := NewIndexMapping() + err := im.UnmarshalJSON([]byte(test.mappingStr)) + if err != nil { + t.Fatalf("failed to unmarshal index mapping: %v", err) + } + + err = im.Validate() + isValid := err == nil + if test.expValidity != isValid { + t.Fatalf("validity mismatch, expected: %v, got: %v", + test.expValidity, isValid) + } + + if !isValid { + errStringMatched := false + for _, possibleErrMsg := range test.errMsgs { + if err.Error() == possibleErrMsg { + errStringMatched = true + break + } + } + if !errStringMatched { + t.Fatalf("invalid error message, expected one of: %v, got: %v", + test.errMsgs, err.Error()) + } + } + }) + } +} + +// A test case for processVector function +type vectorTest struct { + // Input + + ipVec interface{} // input vector + dims int // dimensionality of input vector + + // Expected Output + + expValidity bool // expected validity of the input + expOpVec []float32 // expected output vector, given the input is valid +} + +func TestProcessVector(t *testing.T) { + // Note: while creating vectors, we are using []any instead of []float32, + // this is done to enhance our test coverage. + // When we unmarshal a vector from a JSON, we get []any, not []float32. + tests := []vectorTest{ + // # Flat vectors + + // ## numeric cases + // (all numeric elements) + {[]any{1, 2.2, 3}, 3, true, []float32{1, 2.2, 3}}, // len==dims + {[]any{1, 2.2, 3}, 2, false, nil}, // len>dims + {[]any{1, 2.2, 3}, 4, false, nil}, // lendims + {[]any{[]any{1, 2, 3}}, 2, false, nil}, // len> interleaveShift[0])) & interleaveMagic[1] + b = (b ^ (b >> interleaveShift[1])) & interleaveMagic[2] + b = (b ^ (b >> interleaveShift[2])) & interleaveMagic[3] + b = (b ^ (b >> interleaveShift[3])) & interleaveMagic[4] + b = (b ^ (b >> interleaveShift[4])) & interleaveMagic[5] + return b +} diff --git a/numeric/bin_test.go b/numeric/bin_test.go new file mode 100644 index 0000000..f6dfb47 --- /dev/null +++ b/numeric/bin_test.go @@ -0,0 +1,27 @@ +package numeric + +import "testing" + +func TestInterleaveDeinterleave(t *testing.T) { + tests := []struct { + v1 uint64 + v2 uint64 + }{ + {0, 0}, + {1, 1}, + {27, 39}, + {1<<32 - 1, 1<<32 - 1}, // largest that should still work + } + + for _, test := range tests { + i := Interleave(test.v1, test.v2) + gotv1 := Deinterleave(i) + gotv2 := Deinterleave(i >> 1) + if gotv1 != test.v1 { + t.Errorf("expected v1: %d, got %d, interleaved was %x", test.v1, gotv1, i) + } + if gotv2 != test.v2 { + t.Errorf("expected v2: %d, got %d, interleaved was %x", test.v2, gotv2, i) + } + } +} diff --git a/numeric/float.go b/numeric/float.go new file mode 100644 index 0000000..2bb14d7 --- /dev/null +++ b/numeric/float.go @@ -0,0 +1,34 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package numeric + +import ( + "math" +) + +func Float64ToInt64(f float64) int64 { + fasint := int64(math.Float64bits(f)) + if fasint < 0 { + fasint = fasint ^ 0x7fffffffffffffff + } + return fasint +} + +func Int64ToFloat64(i int64) float64 { + if i < 0 { + i ^= 0x7fffffffffffffff + } + return math.Float64frombits(uint64(i)) +} diff --git a/numeric/float_test.go b/numeric/float_test.go new file mode 100644 index 0000000..b5e99a9 --- /dev/null +++ b/numeric/float_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package numeric + +import ( + "testing" +) + +// test that the float/sortable int operations work both ways +// and that the corresponding integers sort the same as +// the original floats would have +func TestSortabledFloat64ToInt64(t *testing.T) { + tests := []struct { + input float64 + }{ + { + input: -4640094584139352638, + }, + { + input: -167.42, + }, + { + input: -1.11, + }, + { + input: 0, + }, + { + input: 3.14, + }, + { + input: 167.42, + }, + } + + var lastInt64 *int64 + for _, test := range tests { + actual := Float64ToInt64(test.input) + if lastInt64 != nil { + // check that this float is greater than the last one + if actual <= *lastInt64 { + t.Errorf("expected greater than prev, this: %d, last %d", actual, *lastInt64) + } + } + lastInt64 = &actual + convertedBack := Int64ToFloat64(actual) + // assert that we got back what we started with + if convertedBack != test.input { + t.Errorf("expected %f, got %f", test.input, convertedBack) + } + } +} diff --git a/numeric/prefix_coded.go b/numeric/prefix_coded.go new file mode 100644 index 0000000..29bd0fc --- /dev/null +++ b/numeric/prefix_coded.go @@ -0,0 +1,111 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package numeric + +import "fmt" + +const ShiftStartInt64 byte = 0x20 + +// PrefixCoded is a byte array encoding of +// 64-bit numeric values shifted by 0-63 bits +type PrefixCoded []byte + +func NewPrefixCodedInt64(in int64, shift uint) (PrefixCoded, error) { + rv, _, err := NewPrefixCodedInt64Prealloc(in, shift, nil) + return rv, err +} + +func NewPrefixCodedInt64Prealloc(in int64, shift uint, prealloc []byte) ( + rv PrefixCoded, preallocRest []byte, err error) { + if shift > 63 { + return nil, prealloc, fmt.Errorf("cannot shift %d, must be between 0 and 63", shift) + } + + nChars := ((63 - shift) / 7) + 1 + + size := int(nChars + 1) + if len(prealloc) >= size { + rv = PrefixCoded(prealloc[0:size]) + preallocRest = prealloc[size:] + } else { + rv = make(PrefixCoded, size) + } + + rv[0] = ShiftStartInt64 + byte(shift) + + sortableBits := int64(uint64(in) ^ 0x8000000000000000) + sortableBits = int64(uint64(sortableBits) >> shift) + for nChars > 0 { + // Store 7 bits per byte for compatibility + // with UTF-8 encoding of terms + rv[nChars] = byte(sortableBits & 0x7f) + nChars-- + sortableBits = int64(uint64(sortableBits) >> 7) + } + + return rv, preallocRest, nil +} + +func MustNewPrefixCodedInt64(in int64, shift uint) PrefixCoded { + rv, err := NewPrefixCodedInt64(in, shift) + if err != nil { + panic(err) + } + return rv +} + +// Shift returns the number of bits shifted +// returns 0 if in uninitialized state +func (p PrefixCoded) Shift() (uint, error) { + if len(p) > 0 { + shift := p[0] - ShiftStartInt64 + if shift < 0 || shift < 63 { + return uint(shift), nil + } + } + return 0, fmt.Errorf("invalid prefix coded value") +} + +func (p PrefixCoded) Int64() (int64, error) { + shift, err := p.Shift() + if err != nil { + return 0, err + } + var sortableBits int64 + for _, inbyte := range p[1:] { + sortableBits <<= 7 + sortableBits |= int64(inbyte) + } + return int64(uint64((sortableBits << shift)) ^ 0x8000000000000000), nil +} + +func ValidPrefixCodedTerm(p string) (bool, int) { + return ValidPrefixCodedTermBytes([]byte(p)) +} + +func ValidPrefixCodedTermBytes(p []byte) (bool, int) { + if len(p) > 0 { + if p[0] < ShiftStartInt64 || p[0] > ShiftStartInt64+63 { + return false, 0 + } + shift := p[0] - ShiftStartInt64 + nChars := ((63 - int(shift)) / 7) + 1 + if len(p) != nChars+1 { + return false, 0 + } + return true, int(shift) + } + return false, 0 +} diff --git a/numeric/prefix_coded_test.go b/numeric/prefix_coded_test.go new file mode 100644 index 0000000..9256c91 --- /dev/null +++ b/numeric/prefix_coded_test.go @@ -0,0 +1,158 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package numeric + +import ( + "reflect" + "testing" +) + +var tests = []struct { + input int64 + shift uint + output PrefixCoded +}{ + { + input: 1, + shift: 0, + output: PrefixCoded{0x20, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, + }, + { + input: -1, + shift: 0, + output: PrefixCoded{0x20, 0x0, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f}, + }, + { + input: -94582, + shift: 0, + output: PrefixCoded{0x20, 0x0, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7a, 0x1d, 0xa}, + }, + { + input: 314729851, + shift: 0, + output: PrefixCoded{0x20, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x16, 0x9, 0x4a, 0x7b}, + }, + { + input: 314729851, + shift: 4, + output: PrefixCoded{0x24, 0x8, 0x0, 0x0, 0x0, 0x0, 0x9, 0x30, 0x4c, 0x57}, + }, + { + input: 314729851, + shift: 8, + output: PrefixCoded{0x28, 0x40, 0x0, 0x0, 0x0, 0x0, 0x4b, 0x4, 0x65}, + }, + { + input: 314729851, + shift: 16, + output: PrefixCoded{0x30, 0x20, 0x0, 0x0, 0x0, 0x0, 0x25, 0x42}, + }, + { + input: 314729851, + shift: 32, + output: PrefixCoded{0x40, 0x8, 0x0, 0x0, 0x0, 0x0}, + }, + { + input: 1234729851, + shift: 32, + output: PrefixCoded{0x40, 0x8, 0x0, 0x0, 0x0, 0x0}, + }, +} + +// these array encoding values have been verified manually +// against the lucene implementation +func TestPrefixCoded(t *testing.T) { + + for _, test := range tests { + actual, err := NewPrefixCodedInt64(test.input, test.shift) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected %#v, got %#v", test.output, actual) + } + checkedShift, err := actual.Shift() + if err != nil { + t.Error(err) + } + if checkedShift != test.shift { + t.Errorf("expected %d, got %d", test.shift, checkedShift) + } + // if the shift was 0, make sure we can go back to the original + if test.shift == 0 { + backToLong, err := actual.Int64() + if err != nil { + t.Error(err) + } + if backToLong != test.input { + t.Errorf("expected %v, got %v", test.input, backToLong) + } + } + } +} + +func TestPrefixCodedValid(t *testing.T) { + // all of the shared tests should be valid + for _, test := range tests { + valid, _ := ValidPrefixCodedTerm(string(test.output)) + if !valid { + t.Errorf("expected %s to be valid prefix coded, is not", string(test.output)) + } + } + + invalidTests := []struct { + data PrefixCoded + }{ + // first byte invalid skip (too low) + { + data: PrefixCoded{0x19, 'c', 'a', 't'}, + }, + // first byte invalid skip (too high) + { + data: PrefixCoded{0x20 + 64, 'c'}, + }, + // length of trailing bytes wrong (too long) + { + data: PrefixCoded{0x20, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, + }, + // length of trailing bytes wrong (too short) + { + data: PrefixCoded{0x20 + 63}, + }, + } + + // all of the shared tests should be valid + for _, test := range invalidTests { + valid, _ := ValidPrefixCodedTerm(string(test.data)) + if valid { + t.Errorf("expected %s to be invalid prefix coded, it is", string(test.data)) + } + } +} + +func BenchmarkTestPrefixCoded(b *testing.B) { + + for i := 0; i < b.N; i++ { + for _, test := range tests { + actual, err := NewPrefixCodedInt64(test.input, test.shift) + if err != nil { + b.Error(err) + } + if !reflect.DeepEqual(actual, test.output) { + b.Errorf("expected %#v, got %#v", test.output, actual) + } + } + } +} diff --git a/pre_search.go b/pre_search.go new file mode 100644 index 0000000..3dd7e0f --- /dev/null +++ b/pre_search.go @@ -0,0 +1,170 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "github.com/blevesearch/bleve/v2/search" +) + +// A preSearchResultProcessor processes the data in +// the preSearch result from multiple +// indexes in an alias and merges them together to +// create the final preSearch result +type preSearchResultProcessor interface { + // adds the preSearch result to the processor + add(*SearchResult, string) + // updates the final search result with the finalized + // data from the processor + finalize(*SearchResult) +} + +// ----------------------------------------------------------------------------- +// KNN preSearchResultProcessor for handling KNN presearch results +type knnPreSearchResultProcessor struct { + addFn func(sr *SearchResult, indexName string) + finalizeFn func(sr *SearchResult) +} + +func (k *knnPreSearchResultProcessor) add(sr *SearchResult, indexName string) { + if k.addFn != nil { + k.addFn(sr, indexName) + } +} + +func (k *knnPreSearchResultProcessor) finalize(sr *SearchResult) { + if k.finalizeFn != nil { + k.finalizeFn(sr) + } +} + +// ----------------------------------------------------------------------------- +// Synonym preSearchResultProcessor for handling Synonym presearch results +type synonymPreSearchResultProcessor struct { + finalizedFts search.FieldTermSynonymMap +} + +func newSynonymPreSearchResultProcessor() *synonymPreSearchResultProcessor { + return &synonymPreSearchResultProcessor{} +} + +func (s *synonymPreSearchResultProcessor) add(sr *SearchResult, indexName string) { + // Check if SynonymResult or the synonym data key is nil + if sr.SynonymResult == nil { + return + } + + // Attempt to cast PreSearchResults to FieldTermSynonymMap + + // Merge with finalizedFts or initialize it if nil + if s.finalizedFts == nil { + s.finalizedFts = sr.SynonymResult + } else { + s.finalizedFts.MergeWith(sr.SynonymResult) + } +} + +func (s *synonymPreSearchResultProcessor) finalize(sr *SearchResult) { + // Set the finalized synonym data to the PreSearchResults + if s.finalizedFts != nil { + sr.SynonymResult = s.finalizedFts + } +} + +type bm25PreSearchResultProcessor struct { + docCount float64 // bm25 specific stats + fieldCardinality map[string]int +} + +func newBM25PreSearchResultProcessor() *bm25PreSearchResultProcessor { + return &bm25PreSearchResultProcessor{ + fieldCardinality: make(map[string]int), + } +} + +// TODO How will this work for queries other than term queries? +func (b *bm25PreSearchResultProcessor) add(sr *SearchResult, indexName string) { + if sr.BM25Stats != nil { + b.docCount += sr.BM25Stats.DocCount + for field, cardinality := range sr.BM25Stats.FieldCardinality { + b.fieldCardinality[field] += cardinality + } + } +} + +func (b *bm25PreSearchResultProcessor) finalize(sr *SearchResult) { + sr.BM25Stats = &search.BM25Stats{ + DocCount: b.docCount, + FieldCardinality: b.fieldCardinality, + } +} + +// ----------------------------------------------------------------------------- +// Master struct that can hold any number of presearch result processors +type compositePreSearchResultProcessor struct { + presearchResultProcessors []preSearchResultProcessor +} + +// Implements the add method, which forwards to all the internal processors +func (m *compositePreSearchResultProcessor) add(sr *SearchResult, indexName string) { + for _, p := range m.presearchResultProcessors { + p.add(sr, indexName) + } +} + +// Implements the finalize method, which forwards to all the internal processors +func (m *compositePreSearchResultProcessor) finalize(sr *SearchResult) { + for _, p := range m.presearchResultProcessors { + p.finalize(sr) + } +} + +// ----------------------------------------------------------------------------- +// Function to create the appropriate preSearchResultProcessor(s) +func createPreSearchResultProcessor(req *SearchRequest, flags *preSearchFlags) preSearchResultProcessor { + // return nil for invalid input + if flags == nil || req == nil { + return nil + } + var processors []preSearchResultProcessor + // Add KNN processor if the request has KNN + if flags.knn { + if knnProcessor := newKnnPreSearchResultProcessor(req); knnProcessor != nil { + processors = append(processors, knnProcessor) + } + } + // Add Synonym processor if the request has Synonym + if flags.synonyms { + if synonymProcessor := newSynonymPreSearchResultProcessor(); synonymProcessor != nil { + processors = append(processors, synonymProcessor) + } + } + if flags.bm25 { + if bm25Processtor := newBM25PreSearchResultProcessor(); bm25Processtor != nil { + processors = append(processors, bm25Processtor) + } + } + // Return based on the number of processors, optimizing for the common case of 1 processor + // If there are no processors, return nil + switch len(processors) { + case 0: + return nil + case 1: + return processors[0] + default: + return &compositePreSearchResultProcessor{ + presearchResultProcessors: processors, + } + } +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..93e662b --- /dev/null +++ b/query.go @@ -0,0 +1,290 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "time" + + "github.com/blevesearch/bleve/v2/search/query" +) + +// NewBoolFieldQuery creates a new Query for boolean fields +func NewBoolFieldQuery(val bool) *query.BoolFieldQuery { + return query.NewBoolFieldQuery(val) +} + +// NewBooleanQuery creates a compound Query composed +// of several other Query objects. +// These other query objects are added using the +// AddMust() AddShould() and AddMustNot() methods. +// Result documents must satisfy ALL of the +// must Queries. +// Result documents must satisfy NONE of the must not +// Queries. +// Result documents that ALSO satisfy any of the should +// Queries will score higher. +func NewBooleanQuery() *query.BooleanQuery { + return query.NewBooleanQuery(nil, nil, nil) +} + +// NewConjunctionQuery creates a new compound Query. +// Result documents must satisfy all of the queries. +func NewConjunctionQuery(conjuncts ...query.Query) *query.ConjunctionQuery { + return query.NewConjunctionQuery(conjuncts) +} + +// NewDateRangeQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser configured in the +// +// top-level config.QueryDateTimeParser +// +// Either, but not both endpoints can be nil. +func NewDateRangeQuery(start, end time.Time) *query.DateRangeQuery { + return query.NewDateRangeQuery(start, end) +} + +// NewDateRangeInclusiveQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser configured in the +// +// top-level config.QueryDateTimeParser +// +// Either, but not both endpoints can be nil. +// startInclusive and endInclusive control inclusion of the endpoints. +func NewDateRangeInclusiveQuery(start, end time.Time, startInclusive, endInclusive *bool) *query.DateRangeQuery { + return query.NewDateRangeInclusiveQuery(start, end, startInclusive, endInclusive) +} + +// NewDateRangeStringQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser set using +// +// the DateRangeStringQuery.SetDateTimeParser() method. +// +// If no DateTimeParser is set, then the +// +// top-level config.QueryDateTimeParser +// +// is used. +func NewDateRangeStringQuery(start, end string) *query.DateRangeStringQuery { + return query.NewDateRangeStringQuery(start, end) +} + +// NewDateRangeInclusiveStringQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser set using +// +// the DateRangeStringQuery.SetDateTimeParser() method. +// +// this DateTimeParser is a custom date time parser defined in the index mapping, +// using AddCustomDateTimeParser() method. +// If no DateTimeParser is set, then the +// +// top-level config.QueryDateTimeParser +// +// is used. +// Either, but not both endpoints can be nil. +// startInclusive and endInclusive control inclusion of the endpoints. +func NewDateRangeInclusiveStringQuery(start, end string, startInclusive, endInclusive *bool) *query.DateRangeStringQuery { + return query.NewDateRangeStringInclusiveQuery(start, end, startInclusive, endInclusive) +} + +// NewDisjunctionQuery creates a new compound Query. +// Result documents satisfy at least one Query. +func NewDisjunctionQuery(disjuncts ...query.Query) *query.DisjunctionQuery { + return query.NewDisjunctionQuery(disjuncts) +} + +// NewDocIDQuery creates a new Query object returning indexed documents among +// the specified set. Combine it with ConjunctionQuery to restrict the scope of +// other queries output. +func NewDocIDQuery(ids []string) *query.DocIDQuery { + return query.NewDocIDQuery(ids) +} + +// NewFuzzyQuery creates a new Query which finds +// documents containing terms within a specific +// fuzziness of the specified term. +// The default fuzziness is 1. +// +// The current implementation uses Levenshtein edit +// distance as the fuzziness metric. +func NewFuzzyQuery(term string) *query.FuzzyQuery { + return query.NewFuzzyQuery(term) +} + +// NewMatchAllQuery creates a Query which will +// match all documents in the index. +func NewMatchAllQuery() *query.MatchAllQuery { + return query.NewMatchAllQuery() +} + +// NewMatchNoneQuery creates a Query which will not +// match any documents in the index. +func NewMatchNoneQuery() *query.MatchNoneQuery { + return query.NewMatchNoneQuery() +} + +// NewMatchPhraseQuery creates a new Query object +// for matching phrases in the index. +// An Analyzer is chosen based on the field. +// Input text is analyzed using this analyzer. +// Token terms resulting from this analysis are +// used to build a search phrase. Result documents +// must match this phrase. Queried field must have been indexed with +// IncludeTermVectors set to true. +func NewMatchPhraseQuery(matchPhrase string) *query.MatchPhraseQuery { + return query.NewMatchPhraseQuery(matchPhrase) +} + +// NewMatchQuery creates a Query for matching text. +// An Analyzer is chosen based on the field. +// Input text is analyzed using this analyzer. +// Token terms resulting from this analysis are +// used to perform term searches. Result documents +// must satisfy at least one of these term searches. +func NewMatchQuery(match string) *query.MatchQuery { + return query.NewMatchQuery(match) +} + +// NewNumericRangeQuery creates a new Query for ranges +// of numeric values. +// Either, but not both endpoints can be nil. +// The minimum value is inclusive. +// The maximum value is exclusive. +func NewNumericRangeQuery(min, max *float64) *query.NumericRangeQuery { + return query.NewNumericRangeQuery(min, max) +} + +// NewNumericRangeInclusiveQuery creates a new Query for ranges +// of numeric values. +// Either, but not both endpoints can be nil. +// Control endpoint inclusion with inclusiveMin, inclusiveMax. +func NewNumericRangeInclusiveQuery(min, max *float64, minInclusive, maxInclusive *bool) *query.NumericRangeQuery { + return query.NewNumericRangeInclusiveQuery(min, max, minInclusive, maxInclusive) +} + +// NewTermRangeQuery creates a new Query for ranges +// of text terms. +// Either, but not both endpoints can be "". +// The minimum value is inclusive. +// The maximum value is exclusive. +func NewTermRangeQuery(min, max string) *query.TermRangeQuery { + return query.NewTermRangeQuery(min, max) +} + +// NewTermRangeInclusiveQuery creates a new Query for ranges +// of text terms. +// Either, but not both endpoints can be "". +// Control endpoint inclusion with inclusiveMin, inclusiveMax. +func NewTermRangeInclusiveQuery(min, max string, minInclusive, maxInclusive *bool) *query.TermRangeQuery { + return query.NewTermRangeInclusiveQuery(min, max, minInclusive, maxInclusive) +} + +// NewPhraseQuery creates a new Query for finding +// exact term phrases in the index. +// The provided terms must exist in the correct +// order, at the correct index offsets, in the +// specified field. Queried field must have been indexed with +// IncludeTermVectors set to true. +func NewPhraseQuery(terms []string, field string) *query.PhraseQuery { + return query.NewPhraseQuery(terms, field) +} + +// NewPrefixQuery creates a new Query which finds +// documents containing terms that start with the +// specified prefix. +func NewPrefixQuery(prefix string) *query.PrefixQuery { + return query.NewPrefixQuery(prefix) +} + +// NewRegexpQuery creates a new Query which finds +// documents containing terms that match the +// specified regular expression. +func NewRegexpQuery(regexp string) *query.RegexpQuery { + return query.NewRegexpQuery(regexp) +} + +// NewQueryStringQuery creates a new Query used for +// finding documents that satisfy a query string. The +// query string is a small query language for humans. +func NewQueryStringQuery(q string) *query.QueryStringQuery { + return query.NewQueryStringQuery(q) +} + +// NewTermQuery creates a new Query for finding an +// exact term match in the index. +func NewTermQuery(term string) *query.TermQuery { + return query.NewTermQuery(term) +} + +// NewWildcardQuery creates a new Query which finds +// documents containing terms that match the +// specified wildcard. In the wildcard pattern '*' +// will match any sequence of 0 or more characters, +// and '?' will match any single character. +func NewWildcardQuery(wildcard string) *query.WildcardQuery { + return query.NewWildcardQuery(wildcard) +} + +// NewGeoBoundingBoxQuery creates a new Query for performing geo bounding +// box searches. The arguments describe the position of the box and documents +// which have an indexed geo point inside the box will be returned. +func NewGeoBoundingBoxQuery(topLeftLon, topLeftLat, bottomRightLon, bottomRightLat float64) *query.GeoBoundingBoxQuery { + return query.NewGeoBoundingBoxQuery(topLeftLon, topLeftLat, bottomRightLon, bottomRightLat) +} + +// NewGeoDistanceQuery creates a new Query for performing geo distance +// searches. The arguments describe a position and a distance. Documents +// which have an indexed geo point which is less than or equal to the provided +// distance from the given position will be returned. +func NewGeoDistanceQuery(lon, lat float64, distance string) *query.GeoDistanceQuery { + return query.NewGeoDistanceQuery(lon, lat, distance) +} + +// NewIPRangeQuery creates a new Query for matching IP addresses. +// If the argument is in CIDR format, then the query will match all +// IP addresses in the network specified. If the argument is an IP address, +// then the query will return documents which contain that IP. +// Both ipv4 and ipv6 are supported. +func NewIPRangeQuery(cidr string) *query.IPRangeQuery { + return query.NewIPRangeQuery(cidr) +} + +// NewGeoShapeQuery creates a new Query for matching the given geo shape. +// This method can be used for creating geoshape queries for shape types +// like: point, linestring, polygon, multipoint, multilinestring, +// multipolygon and envelope. +func NewGeoShapeQuery(coordinates [][][][]float64, typ, relation string) (*query.GeoShapeQuery, error) { + return query.NewGeoShapeQuery(coordinates, typ, relation) +} + +// NewGeoShapeCircleQuery creates a new query for a geoshape that is a +// circle given center point and the radius. Radius formats supported: +// "5in" "5inch" "7yd" "7yards" "9ft" "9feet" "11km" "11kilometers" +// "3nm" "3nauticalmiles" "13mm" "13millimeters" "15cm" "15centimeters" +// "17mi" "17miles" "19m" "19meters" If the unit cannot be determined, +// the entire string is parsed and the unit of meters is assumed. +func NewGeoShapeCircleQuery(coordinates []float64, radius, relation string) (*query.GeoShapeQuery, error) { + return query.NewGeoShapeCircleQuery(coordinates, radius, relation) +} + +// NewGeometryCollectionQuery creates a new query for the provided +// geometrycollection coordinates and types, which could contain +// multiple geo shapes. +func NewGeometryCollectionQuery(coordinates [][][][][]float64, types []string, relation string) (*query.GeoShapeQuery, error) { + return query.NewGeometryCollectionQuery(coordinates, types, relation) +} diff --git a/query_bench_test.go b/query_bench_test.go new file mode 100644 index 0000000..436f2ea --- /dev/null +++ b/query_bench_test.go @@ -0,0 +1,393 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "strconv" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" + "github.com/blevesearch/bleve/v2/mapping" +) + +func BenchmarkQueryTerm(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + fm := mapping.NewTextFieldMapping() + fm.Analyzer = keyword.Name + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("text", fm) + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + + idx, err := New(tmpIndexPath, imap) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + b.Fatal(err) + } + }() + + members := []string{"abc", "abcdef", "ghi", "jkl", "jklmno"} + for i := 0; i < 100; i++ { + if err = idx.Index(strconv.Itoa(i), + map[string]interface{}{"text": members[i%len(members)]}); err != nil { + b.Fatal(err) + } + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + q := NewTermQuery(members[i%len(members)]) + q.SetField("text") + req := NewSearchRequest(q) + if _, err = idx.Search(req); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkQueryTermRange(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + fm := mapping.NewTextFieldMapping() + fm.Analyzer = keyword.Name + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("text", fm) + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + + idx, err := New(tmpIndexPath, imap) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + b.Fatal(err) + } + }() + + members := []string{"abc", "abcdef", "ghi", "jkl", "jklmno"} + for i := 0; i < 100; i++ { + if err = idx.Index(strconv.Itoa(i), + map[string]interface{}{"text": members[i%len(members)]}); err != nil { + b.Fatal(err) + } + } + + b.ReportAllocs() + b.ResetTimer() + + inclusive := true + for i := 0; i < b.N; i++ { + q := NewTermRangeInclusiveQuery( + members[i%(len(members)-2)], + members[(i+2)%(len(members)-2)], + &inclusive, + &inclusive, + ) + q.SetField("text") + req := NewSearchRequest(q) + if _, err = idx.Search(req); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkQueryWildcard(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + fm := mapping.NewTextFieldMapping() + fm.Analyzer = keyword.Name + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("text", fm) + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + + idx, err := New(tmpIndexPath, imap) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + b.Fatal(err) + } + }() + + members := []string{"abc", "abcdef", "ghi", "jkl", "jklmno"} + for i := 0; i < 100; i++ { + if err = idx.Index(strconv.Itoa(i), + map[string]interface{}{"text": members[i%len(members)]}); err != nil { + b.Fatal(err) + } + } + + wildcards := []string{"ab*", "jk*"} + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + q := NewWildcardQuery(wildcards[i%len(wildcards)]) + q.SetField("text") + req := NewSearchRequest(q) + if _, err = idx.Search(req); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkQueryNumericRange(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + fm := mapping.NewNumericFieldMapping() + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("number", fm) + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + + idx, err := New(tmpIndexPath, imap) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + b.Fatal(err) + } + }() + + for i := 0; i < 100; i++ { + if err = idx.Index(strconv.Itoa(i), + map[string]interface{}{"number": i}); err != nil { + b.Fatal(err) + } + } + + b.ReportAllocs() + b.ResetTimer() + + inclusive := true + for i := 0; i < b.N; i++ { + start := float64(i % 90) + end := float64((i + 10) % 90) + q := NewNumericRangeInclusiveQuery(&start, &end, &inclusive, &inclusive) + q.SetField("number") + req := NewSearchRequest(q) + if _, err = idx.Search(req); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkQueryDateRange(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + fm := mapping.NewDateTimeFieldMapping() + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("date", fm) + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + + idx, err := New(tmpIndexPath, imap) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + b.Fatal(err) + } + }() + + members := []string{ + "2022-11-16T18:45:45Z", + "2022-11-17T18:45:45Z", + "2022-11-18T18:45:45Z", + "2022-11-19T18:45:45Z", + "2022-11-20T18:45:45Z", + } + for i := 0; i < 100; i++ { + if err = idx.Index(strconv.Itoa(i), + map[string]interface{}{"date": members[i%len(members)]}); err != nil { + b.Fatal(err) + } + } + + b.ReportAllocs() + b.ResetTimer() + + inclusive := true + for i := 0; i < b.N; i++ { + start, _ := time.Parse("2006-01-02T15:04:05Z", members[i%(len(members)-2)]) + end, _ := time.Parse("2006-01-02T15:04:05Z", members[(i+2)%(len(members)-2)]) + q := NewDateRangeInclusiveQuery(start, end, &inclusive, &inclusive) + q.SetField("date") + req := NewSearchRequest(q) + if _, err = idx.Search(req); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkQueryGeoDistance(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + fm := mapping.NewGeoPointFieldMapping() + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("geo", fm) + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + + idx, err := New(tmpIndexPath, imap) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + b.Fatal(err) + } + }() + + members := [][]float64{ + {-121.96713072883645, 37.380331474621045}, + {-97.75518866579938, 30.38974491308761}, + {-0.08653451918110022, 51.51063984942306}, + {-2.230759791360498, 53.481514330841236}, + {77.59542326042589, 12.97215865921956}, + } + for i := 0; i < 100; i++ { + if err = idx.Index(strconv.Itoa(i), + map[string]interface{}{"geo": members[i%len(members)]}); err != nil { + b.Fatal(err) + } + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + coordinates := members[i%len(members)] + q := NewGeoDistanceQuery(coordinates[0], coordinates[1], "1mi") + q.SetField("geo") + req := NewSearchRequest(q) + if _, err = idx.Search(req); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkQueryGeoBoundingBox(b *testing.B) { + tmpIndexPath := createTmpIndexPath(b) + defer cleanupTmpIndexPath(b, tmpIndexPath) + + fm := mapping.NewGeoPointFieldMapping() + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("geo", fm) + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + + idx, err := New(tmpIndexPath, imap) + if err != nil { + b.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + b.Fatal(err) + } + }() + + members := [][]float64{ + {-121.96713072883645, 37.380331474621045}, + {-97.75518866579938, 30.38974491308761}, + {-0.08653451918110022, 51.51063984942306}, + {-2.230759791360498, 53.481514330841236}, + {77.59542326042589, 12.97215865921956}, + } + for i := 0; i < 100; i++ { + if err = idx.Index(strconv.Itoa(i), + map[string]interface{}{"geo": members[i%len(members)]}); err != nil { + b.Fatal(err) + } + } + + boundingBoxes := []struct { + topLeft []float64 + bottomRight []float64 + }{ + { + topLeft: []float64{-122.14424992609722, 37.49751487670511}, + bottomRight: []float64{-121.78076546622579, 37.26963069737202}, + }, + { + topLeft: []float64{-97.85362236226437, 30.473743975245725}, + bottomRight: []float64{-97.58691085968482, 30.285211697102895}, + }, + { + topLeft: []float64{-0.28538822102223094, 51.61106497119687}, + bottomRight: []float64{0.16776748108466677, 51.395702237541286}, + }, + { + topLeft: []float64{-2.373683904907921, 53.54371945714075}, + bottomRight: []float64{-2.134365533113197, 53.41788831720595}, + }, + { + topLeft: []float64{77.52617635172015, 13.037587208986437}, + bottomRight: []float64{77.66508989028102, 12.924426170584738}, + }, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + topLeftCoordinates := boundingBoxes[i%len(boundingBoxes)].topLeft + bottomRightCoordinates := boundingBoxes[i%len(boundingBoxes)].bottomRight + q := NewGeoBoundingBoxQuery( + topLeftCoordinates[0], + topLeftCoordinates[1], + bottomRightCoordinates[0], + bottomRightCoordinates[1], + ) + q.SetField("geo") + req := NewSearchRequest(q) + if _, err = idx.Search(req); err != nil { + b.Fatal(err) + } + } +} diff --git a/registry/analyzer.go b/registry/analyzer.go new file mode 100644 index 0000000..af95b88 --- /dev/null +++ b/registry/analyzer.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func RegisterAnalyzer(name string, constructor AnalyzerConstructor) error { + _, exists := analyzers[name] + if exists { + return fmt.Errorf("attempted to register duplicate analyzer named '%s'", name) + } + analyzers[name] = constructor + return nil +} + +type AnalyzerConstructor func(config map[string]interface{}, cache *Cache) (analysis.Analyzer, error) +type AnalyzerRegistry map[string]AnalyzerConstructor + +type AnalyzerCache struct { + *ConcurrentCache +} + +func NewAnalyzerCache() *AnalyzerCache { + return &AnalyzerCache{ + NewConcurrentCache(), + } +} + +func AnalyzerBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := analyzers[name] + if !registered { + return nil, fmt.Errorf("no analyzer with name or type '%s' registered", name) + } + analyzer, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building analyzer: %v", err) + } + return analyzer, nil +} + +func (c *AnalyzerCache) AnalyzerNamed(name string, cache *Cache) (analysis.Analyzer, error) { + item, err := c.ItemNamed(name, cache, AnalyzerBuild) + if err != nil { + return nil, err + } + return item.(analysis.Analyzer), nil +} + +func (c *AnalyzerCache) DefineAnalyzer(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.Analyzer, error) { + item, err := c.DefineItem(name, typ, config, cache, AnalyzerBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("analyzer named '%s' already defined", name) + } + return nil, err + } + return item.(analysis.Analyzer), nil +} + +func AnalyzerTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range analyzers { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/cache.go b/registry/cache.go new file mode 100644 index 0000000..b0ce852 --- /dev/null +++ b/registry/cache.go @@ -0,0 +1,87 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + "sync" +) + +var ErrAlreadyDefined = fmt.Errorf("item already defined") + +type CacheBuild func(name string, config map[string]interface{}, cache *Cache) (interface{}, error) + +type ConcurrentCache struct { + mutex sync.RWMutex + data map[string]interface{} +} + +func NewConcurrentCache() *ConcurrentCache { + return &ConcurrentCache{ + data: make(map[string]interface{}), + } +} + +func (c *ConcurrentCache) ItemNamed(name string, cache *Cache, build CacheBuild) (interface{}, error) { + c.mutex.RLock() + item, cached := c.data[name] + if cached { + c.mutex.RUnlock() + return item, nil + } + // give up read lock + c.mutex.RUnlock() + // try to build it + newItem, err := build(name, nil, cache) + if err != nil { + return nil, err + } + // acquire write lock + c.mutex.Lock() + defer c.mutex.Unlock() + // check again because it could have been created while trading locks + item, cached = c.data[name] + if cached { + return item, nil + } + c.data[name] = newItem + return newItem, nil +} + +func (c *ConcurrentCache) DefineItem(name string, typ string, config map[string]interface{}, cache *Cache, build CacheBuild) (interface{}, error) { + c.mutex.RLock() + _, cached := c.data[name] + if cached { + c.mutex.RUnlock() + return nil, ErrAlreadyDefined + } + // give up read lock so others lookups can proceed + c.mutex.RUnlock() + // really not there, try to build it + newItem, err := build(typ, config, cache) + if err != nil { + return nil, err + } + // now we've built it, acquire lock + c.mutex.Lock() + defer c.mutex.Unlock() + // check again because it could have been created while trading locks + _, cached = c.data[name] + if cached { + return nil, ErrAlreadyDefined + } + c.data[name] = newItem + return newItem, nil +} diff --git a/registry/char_filter.go b/registry/char_filter.go new file mode 100644 index 0000000..e888dac --- /dev/null +++ b/registry/char_filter.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func RegisterCharFilter(name string, constructor CharFilterConstructor) error { + _, exists := charFilters[name] + if exists { + return fmt.Errorf("attempted to register duplicate char filter named '%s'", name) + } + charFilters[name] = constructor + return nil +} + +type CharFilterConstructor func(config map[string]interface{}, cache *Cache) (analysis.CharFilter, error) +type CharFilterRegistry map[string]CharFilterConstructor + +type CharFilterCache struct { + *ConcurrentCache +} + +func NewCharFilterCache() *CharFilterCache { + return &CharFilterCache{ + NewConcurrentCache(), + } +} + +func CharFilterBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := charFilters[name] + if !registered { + return nil, fmt.Errorf("no char filter with name or type '%s' registered", name) + } + charFilter, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building char filter: %v", err) + } + return charFilter, nil +} + +func (c *CharFilterCache) CharFilterNamed(name string, cache *Cache) (analysis.CharFilter, error) { + item, err := c.ItemNamed(name, cache, CharFilterBuild) + if err != nil { + return nil, err + } + return item.(analysis.CharFilter), nil +} + +func (c *CharFilterCache) DefineCharFilter(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.CharFilter, error) { + item, err := c.DefineItem(name, typ, config, cache, CharFilterBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("char filter named '%s' already defined", name) + } + return nil, err + } + return item.(analysis.CharFilter), nil +} + +func CharFilterTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range charFilters { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/datetime_parser.go b/registry/datetime_parser.go new file mode 100644 index 0000000..ff9a80c --- /dev/null +++ b/registry/datetime_parser.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func RegisterDateTimeParser(name string, constructor DateTimeParserConstructor) error { + _, exists := dateTimeParsers[name] + if exists { + return fmt.Errorf("attempted to register duplicate date time parser named '%s'", name) + } + dateTimeParsers[name] = constructor + return nil +} + +type DateTimeParserConstructor func(config map[string]interface{}, cache *Cache) (analysis.DateTimeParser, error) +type DateTimeParserRegistry map[string]DateTimeParserConstructor + +type DateTimeParserCache struct { + *ConcurrentCache +} + +func NewDateTimeParserCache() *DateTimeParserCache { + return &DateTimeParserCache{ + NewConcurrentCache(), + } +} + +func DateTimeParserBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := dateTimeParsers[name] + if !registered { + return nil, fmt.Errorf("no date time parser with name or type '%s' registered", name) + } + dateTimeParser, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building date time parser: %v", err) + } + return dateTimeParser, nil +} + +func (c *DateTimeParserCache) DateTimeParserNamed(name string, cache *Cache) (analysis.DateTimeParser, error) { + item, err := c.ItemNamed(name, cache, DateTimeParserBuild) + if err != nil { + return nil, err + } + return item.(analysis.DateTimeParser), nil +} + +func (c *DateTimeParserCache) DefineDateTimeParser(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.DateTimeParser, error) { + item, err := c.DefineItem(name, typ, config, cache, DateTimeParserBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("date time parser named '%s' already defined", name) + } + return nil, err + } + return item.(analysis.DateTimeParser), nil +} + +func DateTimeParserTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range dateTimeParsers { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/fragment_formatter.go b/registry/fragment_formatter.go new file mode 100644 index 0000000..f32c557 --- /dev/null +++ b/registry/fragment_formatter.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/search/highlight" +) + +func RegisterFragmentFormatter(name string, constructor FragmentFormatterConstructor) error { + _, exists := fragmentFormatters[name] + if exists { + return fmt.Errorf("attempted to register duplicate fragment formatter named '%s'", name) + } + fragmentFormatters[name] = constructor + return nil +} + +type FragmentFormatterConstructor func(config map[string]interface{}, cache *Cache) (highlight.FragmentFormatter, error) +type FragmentFormatterRegistry map[string]FragmentFormatterConstructor + +type FragmentFormatterCache struct { + *ConcurrentCache +} + +func NewFragmentFormatterCache() *FragmentFormatterCache { + return &FragmentFormatterCache{ + NewConcurrentCache(), + } +} + +func FragmentFormatterBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := fragmentFormatters[name] + if !registered { + return nil, fmt.Errorf("no fragment formatter with name or type '%s' registered", name) + } + fragmentFormatter, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building fragment formatter: %v", err) + } + return fragmentFormatter, nil +} + +func (c *FragmentFormatterCache) FragmentFormatterNamed(name string, cache *Cache) (highlight.FragmentFormatter, error) { + item, err := c.ItemNamed(name, cache, FragmentFormatterBuild) + if err != nil { + return nil, err + } + return item.(highlight.FragmentFormatter), nil +} + +func (c *FragmentFormatterCache) DefineFragmentFormatter(name string, typ string, config map[string]interface{}, cache *Cache) (highlight.FragmentFormatter, error) { + item, err := c.DefineItem(name, typ, config, cache, FragmentFormatterBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("fragment formatter named '%s' already defined", name) + } + return nil, err + } + return item.(highlight.FragmentFormatter), nil +} + +func FragmentFormatterTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range fragmentFormatters { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/fragmenter.go b/registry/fragmenter.go new file mode 100644 index 0000000..da2a7b5 --- /dev/null +++ b/registry/fragmenter.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/search/highlight" +) + +func RegisterFragmenter(name string, constructor FragmenterConstructor) error { + _, exists := fragmenters[name] + if exists { + return fmt.Errorf("attempted to register duplicate fragmenter named '%s'", name) + } + fragmenters[name] = constructor + return nil +} + +type FragmenterConstructor func(config map[string]interface{}, cache *Cache) (highlight.Fragmenter, error) +type FragmenterRegistry map[string]FragmenterConstructor + +type FragmenterCache struct { + *ConcurrentCache +} + +func NewFragmenterCache() *FragmenterCache { + return &FragmenterCache{ + NewConcurrentCache(), + } +} + +func FragmenterBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := fragmenters[name] + if !registered { + return nil, fmt.Errorf("no fragmenter with name or type '%s' registered", name) + } + fragmenter, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building fragmenter: %v", err) + } + return fragmenter, nil +} + +func (c *FragmenterCache) FragmenterNamed(name string, cache *Cache) (highlight.Fragmenter, error) { + item, err := c.ItemNamed(name, cache, FragmenterBuild) + if err != nil { + return nil, err + } + return item.(highlight.Fragmenter), nil +} + +func (c *FragmenterCache) DefineFragmenter(name string, typ string, config map[string]interface{}, cache *Cache) (highlight.Fragmenter, error) { + item, err := c.DefineItem(name, typ, config, cache, FragmenterBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("fragmenter named '%s' already defined", name) + } + return nil, err + } + return item.(highlight.Fragmenter), nil +} + +func FragmenterTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range fragmenters { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/highlighter.go b/registry/highlighter.go new file mode 100644 index 0000000..75de254 --- /dev/null +++ b/registry/highlighter.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/search/highlight" +) + +func RegisterHighlighter(name string, constructor HighlighterConstructor) error { + _, exists := highlighters[name] + if exists { + return fmt.Errorf("attempted to register duplicate highlighter named '%s'", name) + } + highlighters[name] = constructor + return nil +} + +type HighlighterConstructor func(config map[string]interface{}, cache *Cache) (highlight.Highlighter, error) +type HighlighterRegistry map[string]HighlighterConstructor + +type HighlighterCache struct { + *ConcurrentCache +} + +func NewHighlighterCache() *HighlighterCache { + return &HighlighterCache{ + NewConcurrentCache(), + } +} + +func HighlighterBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := highlighters[name] + if !registered { + return nil, fmt.Errorf("no highlighter with name or type '%s' registered", name) + } + highlighter, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building highlighter: %v", err) + } + return highlighter, nil +} + +func (c *HighlighterCache) HighlighterNamed(name string, cache *Cache) (highlight.Highlighter, error) { + item, err := c.ItemNamed(name, cache, HighlighterBuild) + if err != nil { + return nil, err + } + return item.(highlight.Highlighter), nil +} + +func (c *HighlighterCache) DefineHighlighter(name string, typ string, config map[string]interface{}, cache *Cache) (highlight.Highlighter, error) { + item, err := c.DefineItem(name, typ, config, cache, HighlighterBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("highlighter named '%s' already defined", name) + } + return nil, err + } + return item.(highlight.Highlighter), nil +} + +func HighlighterTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range highlighters { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/index_type.go b/registry/index_type.go new file mode 100644 index 0000000..0c2c87f --- /dev/null +++ b/registry/index_type.go @@ -0,0 +1,46 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + index "github.com/blevesearch/bleve_index_api" +) + +func RegisterIndexType(name string, constructor IndexTypeConstructor) error { + _, exists := indexTypes[name] + if exists { + return fmt.Errorf("attempted to register duplicate index encoding named '%s'", name) + } + indexTypes[name] = constructor + return nil +} + +type IndexTypeConstructor func(storeName string, storeConfig map[string]interface{}, analysisQueue *index.AnalysisQueue) (index.Index, error) +type IndexTypeRegistry map[string]IndexTypeConstructor + +func IndexTypeConstructorByName(name string) IndexTypeConstructor { + return indexTypes[name] +} + +func IndexTypesAndInstances() ([]string, []string) { + var types []string + var instances []string + for name := range indexTypes { + types = append(types, name) + } + return types, instances +} diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 0000000..69ee8dd --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,195 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +var stores = make(KVStoreRegistry, 0) +var indexTypes = make(IndexTypeRegistry, 0) + +// highlight +var fragmentFormatters = make(FragmentFormatterRegistry, 0) +var fragmenters = make(FragmenterRegistry, 0) +var highlighters = make(HighlighterRegistry, 0) + +// analysis +var charFilters = make(CharFilterRegistry, 0) +var tokenizers = make(TokenizerRegistry, 0) +var tokenMaps = make(TokenMapRegistry, 0) +var tokenFilters = make(TokenFilterRegistry, 0) +var analyzers = make(AnalyzerRegistry, 0) +var dateTimeParsers = make(DateTimeParserRegistry, 0) +var synonymSources = make(SynonymSourceRegistry, 0) + +type Cache struct { + CharFilters *CharFilterCache + Tokenizers *TokenizerCache + TokenMaps *TokenMapCache + TokenFilters *TokenFilterCache + Analyzers *AnalyzerCache + DateTimeParsers *DateTimeParserCache + FragmentFormatters *FragmentFormatterCache + Fragmenters *FragmenterCache + Highlighters *HighlighterCache + SynonymSources *SynonymSourceCache +} + +func NewCache() *Cache { + return &Cache{ + CharFilters: NewCharFilterCache(), + Tokenizers: NewTokenizerCache(), + TokenMaps: NewTokenMapCache(), + TokenFilters: NewTokenFilterCache(), + Analyzers: NewAnalyzerCache(), + DateTimeParsers: NewDateTimeParserCache(), + FragmentFormatters: NewFragmentFormatterCache(), + Fragmenters: NewFragmenterCache(), + Highlighters: NewHighlighterCache(), + SynonymSources: NewSynonymSourceCache(), + } +} + +func typeFromConfig(config map[string]interface{}) (string, error) { + prop, ok := config["type"] + if !ok { + return "", fmt.Errorf("'type' property is not defined") + } + typ, ok := prop.(string) + if !ok { + return "", fmt.Errorf("'type' property must be a string, not %T", prop) + } + return typ, nil +} + +func (c *Cache) CharFilterNamed(name string) (analysis.CharFilter, error) { + return c.CharFilters.CharFilterNamed(name, c) +} + +func (c *Cache) DefineCharFilter(name string, config map[string]interface{}) (analysis.CharFilter, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.CharFilters.DefineCharFilter(name, typ, config, c) +} + +func (c *Cache) TokenizerNamed(name string) (analysis.Tokenizer, error) { + return c.Tokenizers.TokenizerNamed(name, c) +} + +func (c *Cache) DefineTokenizer(name string, config map[string]interface{}) (analysis.Tokenizer, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, fmt.Errorf("cannot resolve '%s' tokenizer type: %s", name, err) + } + return c.Tokenizers.DefineTokenizer(name, typ, config, c) +} + +func (c *Cache) TokenMapNamed(name string) (analysis.TokenMap, error) { + return c.TokenMaps.TokenMapNamed(name, c) +} + +func (c *Cache) DefineTokenMap(name string, config map[string]interface{}) (analysis.TokenMap, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.TokenMaps.DefineTokenMap(name, typ, config, c) +} + +func (c *Cache) TokenFilterNamed(name string) (analysis.TokenFilter, error) { + return c.TokenFilters.TokenFilterNamed(name, c) +} + +func (c *Cache) DefineTokenFilter(name string, config map[string]interface{}) (analysis.TokenFilter, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.TokenFilters.DefineTokenFilter(name, typ, config, c) +} + +func (c *Cache) AnalyzerNamed(name string) (analysis.Analyzer, error) { + return c.Analyzers.AnalyzerNamed(name, c) +} + +func (c *Cache) DefineAnalyzer(name string, config map[string]interface{}) (analysis.Analyzer, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.Analyzers.DefineAnalyzer(name, typ, config, c) +} + +func (c *Cache) DateTimeParserNamed(name string) (analysis.DateTimeParser, error) { + return c.DateTimeParsers.DateTimeParserNamed(name, c) +} + +func (c *Cache) DefineDateTimeParser(name string, config map[string]interface{}) (analysis.DateTimeParser, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.DateTimeParsers.DefineDateTimeParser(name, typ, config, c) +} + +func (c *Cache) SynonymSourceNamed(name string) (analysis.SynonymSource, error) { + return c.SynonymSources.SynonymSourceNamed(name, c) +} + +func (c *Cache) DefineSynonymSource(name string, config map[string]interface{}) (analysis.SynonymSource, error) { + return c.SynonymSources.DefineSynonymSource(name, analysis.SynonymSourceType, config, c) +} + +func (c *Cache) FragmentFormatterNamed(name string) (highlight.FragmentFormatter, error) { + return c.FragmentFormatters.FragmentFormatterNamed(name, c) +} + +func (c *Cache) DefineFragmentFormatter(name string, config map[string]interface{}) (highlight.FragmentFormatter, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.FragmentFormatters.DefineFragmentFormatter(name, typ, config, c) +} + +func (c *Cache) FragmenterNamed(name string) (highlight.Fragmenter, error) { + return c.Fragmenters.FragmenterNamed(name, c) +} + +func (c *Cache) DefineFragmenter(name string, config map[string]interface{}) (highlight.Fragmenter, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.Fragmenters.DefineFragmenter(name, typ, config, c) +} + +func (c *Cache) HighlighterNamed(name string) (highlight.Highlighter, error) { + return c.Highlighters.HighlighterNamed(name, c) +} + +func (c *Cache) DefineHighlighter(name string, config map[string]interface{}) (highlight.Highlighter, error) { + typ, err := typeFromConfig(config) + if err != nil { + return nil, err + } + return c.Highlighters.DefineHighlighter(name, typ, config, c) +} diff --git a/registry/store.go b/registry/store.go new file mode 100644 index 0000000..5684083 --- /dev/null +++ b/registry/store.go @@ -0,0 +1,52 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + store "github.com/blevesearch/upsidedown_store_api" +) + +func RegisterKVStore(name string, constructor KVStoreConstructor) error { + _, exists := stores[name] + if exists { + return fmt.Errorf("attempted to register duplicate store named '%s'", name) + } + stores[name] = constructor + return nil +} + +// KVStoreConstructor is used to build a KVStore of a specific type when +// specificied by the index configuration. In addition to meeting the +// store.KVStore interface, KVStores must also support this constructor. +// Note that currently the values of config must +// be able to be marshaled and unmarshaled using the encoding/json library (used +// when reading/writing the index metadata file). +type KVStoreConstructor func(mo store.MergeOperator, config map[string]interface{}) (store.KVStore, error) +type KVStoreRegistry map[string]KVStoreConstructor + +func KVStoreConstructorByName(name string) KVStoreConstructor { + return stores[name] +} + +func KVStoreTypesAndInstances() ([]string, []string) { + var types []string + var instances []string + for name := range stores { + types = append(types, name) + } + return types, instances +} diff --git a/registry/synonym_source.go b/registry/synonym_source.go new file mode 100644 index 0000000..f1836f8 --- /dev/null +++ b/registry/synonym_source.go @@ -0,0 +1,86 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func RegisterSynonymSource(typ string, constructor SynonymSourceConstructor) error { + _, exists := synonymSources[typ] + if exists { + return fmt.Errorf("attempted to register duplicate synonym source with type '%s'", typ) + } + synonymSources[typ] = constructor + return nil +} + +type SynonymSourceCache struct { + *ConcurrentCache +} + +func NewSynonymSourceCache() *SynonymSourceCache { + return &SynonymSourceCache{ + NewConcurrentCache(), + } +} + +type SynonymSourceConstructor func(config map[string]interface{}, cache *Cache) (analysis.SynonymSource, error) +type SynonymSourceRegistry map[string]SynonymSourceConstructor + +func SynonymSourceBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := synonymSources[name] + if !registered { + return nil, fmt.Errorf("no synonym source with name '%s' registered", name) + } + synonymSource, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building synonym source: %v", err) + } + return synonymSource, nil +} + +func (c *SynonymSourceCache) SynonymSourceNamed(name string, cache *Cache) (analysis.SynonymSource, error) { + item, err := c.ItemNamed(name, cache, SynonymSourceBuild) + if err != nil { + return nil, err + } + return item.(analysis.SynonymSource), nil +} + +func (c *SynonymSourceCache) DefineSynonymSource(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.SynonymSource, error) { + item, err := c.DefineItem(name, typ, config, cache, SynonymSourceBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("synonym source named '%s' already defined", name) + } + return nil, err + } + return item.(analysis.SynonymSource), nil +} + +func (c *SynonymSourceCache) VisitSynonymSources(visitor analysis.SynonymSourceVisitor) error { + c.mutex.RLock() + defer c.mutex.RUnlock() + for k, v := range c.data { + err := visitor(k, v.(analysis.SynonymSource)) + if err != nil { + return err + } + } + return nil +} diff --git a/registry/token_filter.go b/registry/token_filter.go new file mode 100644 index 0000000..533a103 --- /dev/null +++ b/registry/token_filter.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func RegisterTokenFilter(name string, constructor TokenFilterConstructor) error { + _, exists := tokenFilters[name] + if exists { + return fmt.Errorf("attempted to register duplicate token filter named '%s'", name) + } + tokenFilters[name] = constructor + return nil +} + +type TokenFilterConstructor func(config map[string]interface{}, cache *Cache) (analysis.TokenFilter, error) +type TokenFilterRegistry map[string]TokenFilterConstructor + +type TokenFilterCache struct { + *ConcurrentCache +} + +func NewTokenFilterCache() *TokenFilterCache { + return &TokenFilterCache{ + NewConcurrentCache(), + } +} + +func TokenFilterBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := tokenFilters[name] + if !registered { + return nil, fmt.Errorf("no token filter with name or type '%s' registered", name) + } + tokenFilter, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building token filter: %v", err) + } + return tokenFilter, nil +} + +func (c *TokenFilterCache) TokenFilterNamed(name string, cache *Cache) (analysis.TokenFilter, error) { + item, err := c.ItemNamed(name, cache, TokenFilterBuild) + if err != nil { + return nil, err + } + return item.(analysis.TokenFilter), nil +} + +func (c *TokenFilterCache) DefineTokenFilter(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.TokenFilter, error) { + item, err := c.DefineItem(name, typ, config, cache, TokenFilterBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("token filter named '%s' already defined", name) + } + return nil, err + } + return item.(analysis.TokenFilter), nil +} + +func TokenFilterTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range tokenFilters { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/token_maps.go b/registry/token_maps.go new file mode 100644 index 0000000..7fd7886 --- /dev/null +++ b/registry/token_maps.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func RegisterTokenMap(name string, constructor TokenMapConstructor) error { + _, exists := tokenMaps[name] + if exists { + return fmt.Errorf("attempted to register duplicate token map named '%s'", name) + } + tokenMaps[name] = constructor + return nil +} + +type TokenMapConstructor func(config map[string]interface{}, cache *Cache) (analysis.TokenMap, error) +type TokenMapRegistry map[string]TokenMapConstructor + +type TokenMapCache struct { + *ConcurrentCache +} + +func NewTokenMapCache() *TokenMapCache { + return &TokenMapCache{ + NewConcurrentCache(), + } +} + +func TokenMapBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := tokenMaps[name] + if !registered { + return nil, fmt.Errorf("no token map with name or type '%s' registered", name) + } + tokenMap, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building token map: %v", err) + } + return tokenMap, nil +} + +func (c *TokenMapCache) TokenMapNamed(name string, cache *Cache) (analysis.TokenMap, error) { + item, err := c.ItemNamed(name, cache, TokenMapBuild) + if err != nil { + return nil, err + } + return item.(analysis.TokenMap), nil +} + +func (c *TokenMapCache) DefineTokenMap(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.TokenMap, error) { + item, err := c.DefineItem(name, typ, config, cache, TokenMapBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("token map named '%s' already defined", name) + } + return nil, err + } + return item.(analysis.TokenMap), nil +} + +func TokenMapTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range tokenMaps { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/registry/tokenizer.go b/registry/tokenizer.go new file mode 100644 index 0000000..81222b8 --- /dev/null +++ b/registry/tokenizer.go @@ -0,0 +1,90 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func RegisterTokenizer(name string, constructor TokenizerConstructor) error { + _, exists := tokenizers[name] + if exists { + return fmt.Errorf("attempted to register duplicate tokenizer named '%s'", name) + } + tokenizers[name] = constructor + return nil +} + +type TokenizerConstructor func(config map[string]interface{}, cache *Cache) (analysis.Tokenizer, error) +type TokenizerRegistry map[string]TokenizerConstructor + +type TokenizerCache struct { + *ConcurrentCache +} + +func NewTokenizerCache() *TokenizerCache { + return &TokenizerCache{ + NewConcurrentCache(), + } +} + +func TokenizerBuild(name string, config map[string]interface{}, cache *Cache) (interface{}, error) { + cons, registered := tokenizers[name] + if !registered { + return nil, fmt.Errorf("no tokenizer with name or type '%s' registered", name) + } + tokenizer, err := cons(config, cache) + if err != nil { + return nil, fmt.Errorf("error building tokenizer: %v", err) + } + return tokenizer, nil +} + +func (c *TokenizerCache) TokenizerNamed(name string, cache *Cache) (analysis.Tokenizer, error) { + item, err := c.ItemNamed(name, cache, TokenizerBuild) + if err != nil { + return nil, err + } + return item.(analysis.Tokenizer), nil +} + +func (c *TokenizerCache) DefineTokenizer(name string, typ string, config map[string]interface{}, cache *Cache) (analysis.Tokenizer, error) { + item, err := c.DefineItem(name, typ, config, cache, TokenizerBuild) + if err != nil { + if err == ErrAlreadyDefined { + return nil, fmt.Errorf("tokenizer named '%s' already defined", name) + } + return nil, err + } + return item.(analysis.Tokenizer), nil +} + +func TokenizerTypesAndInstances() ([]string, []string) { + emptyConfig := map[string]interface{}{} + emptyCache := NewCache() + var types []string + var instances []string + for name, cons := range tokenizers { + _, err := cons(emptyConfig, emptyCache) + if err == nil { + instances = append(instances, name) + } else { + types = append(types, name) + } + } + return types, instances +} diff --git a/scripts/build_children.sh b/scripts/build_children.sh new file mode 100755 index 0000000..7559ccd --- /dev/null +++ b/scripts/build_children.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# Get last child project build number +BUILD_NUM=$(curl -s 'https://api.travis-ci.org/repos/blevesearch/beer-search/builds' | grep -o '^\[{"id":[0-9]*,' | grep -o '[0-9]' | tr -d '\n') +# Restart last child project build +curl -X POST https://api.travis-ci.org/builds/$BUILD_NUM/restart --header "Authorization: token "$AUTH_TOKEN diff --git a/scripts/merge-coverprofile.go b/scripts/merge-coverprofile.go new file mode 100644 index 0000000..c843819 --- /dev/null +++ b/scripts/merge-coverprofile.go @@ -0,0 +1,62 @@ +// Copyright © 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build ignore +// +build ignore + +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + + modeline := "" + blocks := map[string]int{} + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "mode:") { + lastSpace := strings.LastIndex(line, " ") + prefix := line[0:lastSpace] + suffix := line[lastSpace+1:] + count, err := strconv.Atoi(suffix) + if err != nil { + fmt.Printf("error parsing count: %v", err) + continue + } + existingCount, exists := blocks[prefix] + if exists { + blocks[prefix] = existingCount + count + } else { + blocks[prefix] = count + } + } else if modeline == "" { + modeline = line + } + } + if err := scanner.Err(); err != nil { + fmt.Fprintln(os.Stderr, "reading standard input:", err) + } + + fmt.Println(modeline) + for k, v := range blocks { + fmt.Printf("%s %d\n", k, v) + } +} diff --git a/scripts/old_build_script.txt b/scripts/old_build_script.txt new file mode 100644 index 0000000..2314cd9 --- /dev/null +++ b/scripts/old_build_script.txt @@ -0,0 +1,29 @@ +old build script +# remove old icu +sudo apt-get -y remove libicu48 + +# install snappy +sudo apt-get -y install libsnappy-dev + +# install newer icu +curl -o /tmp/icu4c-53_1-RHEL6-x64.tgz http://download.icu-project.org/files/icu4c/53.1/icu4c-53_1-RHEL6-x64.tgz +sudo tar zxvf /tmp/icu4c-53_1-RHEL6-x64.tgz -C / + +# install leveldb +curl -O https://leveldb.googlecode.com/files/leveldb-1.15.0.tar.gz +tar zxvf leveldb-1.15.0.tar.gz +cd leveldb-1.15.0 +make +sudo cp --preserve=links libleveldb.* /usr/local/lib +sudo cp -r include/leveldb /usr/local/include/ +sudo ldconfig +cd .. + +#install cld2 +cd analysis/token_filters/cld2 +svn checkout http://cld2.googlecode.com/svn/trunk/ cld2-read-only +cd cld2-read-only/internal/ +./compile_libs.sh +sudo cp *.so /usr/local/lib +sudo ldconfig +cd ../../../../.. \ No newline at end of file diff --git a/scripts/project-code-coverage.sh b/scripts/project-code-coverage.sh new file mode 100755 index 0000000..e67f6b4 --- /dev/null +++ b/scripts/project-code-coverage.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +echo "mode: count" > acc.out +for Dir in . $(find ./* -maxdepth 10 -type d | grep -v vendor); +do + if ls $Dir/*.go &> /dev/null; + then + returnval=`go test -coverprofile=profile.out -covermode=count $Dir` + echo ${returnval} + if [[ ${returnval} != *FAIL* ]] + then + if [ -f profile.out ] + then + cat profile.out | grep -v "mode: count" >> acc.out + fi + else + exit 1 + fi + fi +done + +# collect integration test coverage +echo "mode: count" > integration-acc.out +INTPACKS=`go list ./... | grep -v vendor | grep -v utils | grep -v 'store/test' | grep -v docs | xargs | sed 's/ /,/g'` +returnval=`go test -coverpkg=$INTPACKS -coverprofile=profile.out -covermode=count ./test` +if [[ ${returnval} != *FAIL* ]] +then + if [ -f profile.out ] + then + cat profile.out | grep -v "mode: count" >> integration-acc.out + fi +else + exit 1 +fi + +cat acc.out integration-acc.out | go run scripts/merge-coverprofile.go > merged.out + +if [ -n "$COVERALLS" ] +then + export GIT_BRANCH=$TRAVIS_BRANCH + goveralls -service drone.io -coverprofile=merged.out -repotoken $COVERALLS +fi + +if [ -n "$COVERHTML" ] +then + go tool cover -html=merged.out +fi + +rm -rf ./profile.out +rm -rf ./acc.out +rm -rf ./integration-acc.out +rm -rf ./merged.out diff --git a/search.go b/search.go new file mode 100644 index 0000000..2c25e05 --- /dev/null +++ b/search.go @@ -0,0 +1,609 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "fmt" + "reflect" + "sort" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/datetime/optional" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/collector" + "github.com/blevesearch/bleve/v2/search/query" + "github.com/blevesearch/bleve/v2/size" + "github.com/blevesearch/bleve/v2/util" +) + +var ( + reflectStaticSizeSearchResult int + reflectStaticSizeSearchStatus int +) + +func init() { + var sr SearchResult + reflectStaticSizeSearchResult = int(reflect.TypeOf(sr).Size()) + var ss SearchStatus + reflectStaticSizeSearchStatus = int(reflect.TypeOf(ss).Size()) +} + +var cache = registry.NewCache() + +const defaultDateTimeParser = optional.Name + +type dateTimeRange struct { + Name string `json:"name,omitempty"` + Start time.Time `json:"start,omitempty"` + End time.Time `json:"end,omitempty"` + DateTimeParser string `json:"datetime_parser,omitempty"` + startString *string + endString *string +} + +func (dr *dateTimeRange) ParseDates(dateTimeParser analysis.DateTimeParser) (start, end time.Time, err error) { + start = dr.Start + if dr.Start.IsZero() && dr.startString != nil { + s, _, parseError := dateTimeParser.ParseDateTime(*dr.startString) + if parseError != nil { + return start, end, fmt.Errorf("error parsing start date '%s' for date range name '%s': %v", *dr.startString, dr.Name, parseError) + } + start = s + } + end = dr.End + if dr.End.IsZero() && dr.endString != nil { + e, _, parseError := dateTimeParser.ParseDateTime(*dr.endString) + if parseError != nil { + return start, end, fmt.Errorf("error parsing end date '%s' for date range name '%s': %v", *dr.endString, dr.Name, parseError) + } + end = e + } + return start, end, err +} + +func (dr *dateTimeRange) UnmarshalJSON(input []byte) error { + var temp struct { + Name string `json:"name,omitempty"` + Start *string `json:"start,omitempty"` + End *string `json:"end,omitempty"` + DateTimeParser string `json:"datetime_parser,omitempty"` + } + + if err := util.UnmarshalJSON(input, &temp); err != nil { + return err + } + + dr.Name = temp.Name + if temp.Start != nil { + dr.startString = temp.Start + } + if temp.End != nil { + dr.endString = temp.End + } + if temp.DateTimeParser != "" { + dr.DateTimeParser = temp.DateTimeParser + } + + return nil +} + +func (dr *dateTimeRange) MarshalJSON() ([]byte, error) { + rv := map[string]interface{}{ + "name": dr.Name, + } + + if !dr.Start.IsZero() { + rv["start"] = dr.Start + } else if dr.startString != nil { + rv["start"] = dr.startString + } + + if !dr.End.IsZero() { + rv["end"] = dr.End + } else if dr.endString != nil { + rv["end"] = dr.endString + } + + if dr.DateTimeParser != "" { + rv["datetime_parser"] = dr.DateTimeParser + } + return util.MarshalJSON(rv) +} + +type numericRange struct { + Name string `json:"name,omitempty"` + Min *float64 `json:"min,omitempty"` + Max *float64 `json:"max,omitempty"` +} + +// A FacetRequest describes a facet or aggregation +// of the result document set you would like to be +// built. +type FacetRequest struct { + Size int `json:"size"` + Field string `json:"field"` + NumericRanges []*numericRange `json:"numeric_ranges,omitempty"` + DateTimeRanges []*dateTimeRange `json:"date_ranges,omitempty"` +} + +// NewFacetRequest creates a facet on the specified +// field that limits the number of entries to the +// specified size. +func NewFacetRequest(field string, size int) *FacetRequest { + return &FacetRequest{ + Field: field, + Size: size, + } +} + +func (fr *FacetRequest) Validate() error { + nrCount := len(fr.NumericRanges) + drCount := len(fr.DateTimeRanges) + if nrCount > 0 && drCount > 0 { + return fmt.Errorf("facet can only contain numeric ranges or date ranges, not both") + } + + if nrCount > 0 { + nrNames := map[string]interface{}{} + for _, nr := range fr.NumericRanges { + if _, ok := nrNames[nr.Name]; ok { + return fmt.Errorf("numeric ranges contains duplicate name '%s'", nr.Name) + } + nrNames[nr.Name] = struct{}{} + if nr.Min == nil && nr.Max == nil { + return fmt.Errorf("numeric range query must specify either min, max or both for range name '%s'", nr.Name) + } + } + + } else { + dateTimeParser, err := cache.DateTimeParserNamed(defaultDateTimeParser) + if err != nil { + return err + } + drNames := map[string]interface{}{} + for _, dr := range fr.DateTimeRanges { + if _, ok := drNames[dr.Name]; ok { + return fmt.Errorf("date ranges contains duplicate name '%s'", dr.Name) + } + drNames[dr.Name] = struct{}{} + if dr.DateTimeParser == "" { + // cannot parse the date range dates as the defaultDateTimeParser is overridden + // so perform this validation at query time + start, end, err := dr.ParseDates(dateTimeParser) + if err != nil { + return fmt.Errorf("ParseDates err: %v, using date time parser named %s", err, defaultDateTimeParser) + } + if start.IsZero() && end.IsZero() { + return fmt.Errorf("date range query must specify either start, end or both for range name '%s'", dr.Name) + } + } + } + } + + return nil +} + +// AddDateTimeRange adds a bucket to a field +// containing date values. Documents with a +// date value falling into this range are tabulated +// as part of this bucket/range. +func (fr *FacetRequest) AddDateTimeRange(name string, start, end time.Time) { + if fr.DateTimeRanges == nil { + fr.DateTimeRanges = make([]*dateTimeRange, 0, 1) + } + fr.DateTimeRanges = append(fr.DateTimeRanges, &dateTimeRange{Name: name, Start: start, End: end}) +} + +// AddDateTimeRangeString adds a bucket to a field +// containing date values. Uses defaultDateTimeParser to parse the date strings. +func (fr *FacetRequest) AddDateTimeRangeString(name string, start, end *string) { + if fr.DateTimeRanges == nil { + fr.DateTimeRanges = make([]*dateTimeRange, 0, 1) + } + fr.DateTimeRanges = append(fr.DateTimeRanges, + &dateTimeRange{Name: name, startString: start, endString: end}) +} + +// AddDateTimeRangeString adds a bucket to a field +// containing date values. Uses the specified parser to parse the date strings. +// provided the parser is registered in the index mapping. +func (fr *FacetRequest) AddDateTimeRangeStringWithParser(name string, start, end *string, parser string) { + if fr.DateTimeRanges == nil { + fr.DateTimeRanges = make([]*dateTimeRange, 0, 1) + } + fr.DateTimeRanges = append(fr.DateTimeRanges, + &dateTimeRange{Name: name, startString: start, endString: end, DateTimeParser: parser}) +} + +// AddNumericRange adds a bucket to a field +// containing numeric values. Documents with a +// numeric value falling into this range are +// tabulated as part of this bucket/range. +func (fr *FacetRequest) AddNumericRange(name string, min, max *float64) { + if fr.NumericRanges == nil { + fr.NumericRanges = make([]*numericRange, 0, 1) + } + fr.NumericRanges = append(fr.NumericRanges, &numericRange{Name: name, Min: min, Max: max}) +} + +// FacetsRequest groups together all the +// FacetRequest objects for a single query. +type FacetsRequest map[string]*FacetRequest + +func (fr FacetsRequest) Validate() error { + for _, v := range fr { + if err := v.Validate(); err != nil { + return err + } + } + return nil +} + +// HighlightRequest describes how field matches +// should be highlighted. +type HighlightRequest struct { + Style *string `json:"style"` + Fields []string `json:"fields"` +} + +// NewHighlight creates a default +// HighlightRequest. +func NewHighlight() *HighlightRequest { + return &HighlightRequest{} +} + +// NewHighlightWithStyle creates a HighlightRequest +// with an alternate style. +func NewHighlightWithStyle(style string) *HighlightRequest { + return &HighlightRequest{ + Style: &style, + } +} + +func (h *HighlightRequest) AddField(field string) { + if h.Fields == nil { + h.Fields = make([]string, 0, 1) + } + h.Fields = append(h.Fields, field) +} + +func (r *SearchRequest) Validate() error { + if srq, ok := r.Query.(query.ValidatableQuery); ok { + err := srq.Validate() + if err != nil { + return err + } + } + + if r.SearchAfter != nil && r.SearchBefore != nil { + return fmt.Errorf("cannot use search after and search before together") + } + + if r.SearchAfter != nil { + if r.From != 0 { + return fmt.Errorf("cannot use search after with from !=0") + } + if len(r.SearchAfter) != len(r.Sort) { + return fmt.Errorf("search after must have same size as sort order") + } + } + if r.SearchBefore != nil { + if r.From != 0 { + return fmt.Errorf("cannot use search before with from !=0") + } + if len(r.SearchBefore) != len(r.Sort) { + return fmt.Errorf("search before must have same size as sort order") + } + } + + err := validateKNN(r) + if err != nil { + return err + } + return r.Facets.Validate() +} + +// AddFacet adds a FacetRequest to this SearchRequest +func (r *SearchRequest) AddFacet(facetName string, f *FacetRequest) { + if r.Facets == nil { + r.Facets = make(FacetsRequest, 1) + } + r.Facets[facetName] = f +} + +// SortBy changes the request to use the requested sort order +// this form uses the simplified syntax with an array of strings +// each string can either be a field name +// or the magic value _id and _score which refer to the doc id and search score +// any of these values can optionally be prefixed with - to reverse the order +func (r *SearchRequest) SortBy(order []string) { + so := search.ParseSortOrderStrings(order) + r.Sort = so +} + +// SortByCustom changes the request to use the requested sort order +func (r *SearchRequest) SortByCustom(order search.SortOrder) { + r.Sort = order +} + +// SetSearchAfter sets the request to skip over hits with a sort +// value less than the provided sort after key +func (r *SearchRequest) SetSearchAfter(after []string) { + r.SearchAfter = after +} + +// SetSearchBefore sets the request to skip over hits with a sort +// value greater than the provided sort before key +func (r *SearchRequest) SetSearchBefore(before []string) { + r.SearchBefore = before +} + +// NewSearchRequest creates a new SearchRequest +// for the Query, using default values for all +// other search parameters. +func NewSearchRequest(q query.Query) *SearchRequest { + return NewSearchRequestOptions(q, 10, 0, false) +} + +// NewSearchRequestOptions creates a new SearchRequest +// for the Query, with the requested size, from +// and explanation search parameters. +// By default results are ordered by score, descending. +func NewSearchRequestOptions(q query.Query, size, from int, explain bool) *SearchRequest { + return &SearchRequest{ + Query: q, + Size: size, + From: from, + Explain: explain, + Sort: search.SortOrder{&search.SortScore{Desc: true}}, + } +} + +// IndexErrMap tracks errors with the name of the index where it occurred +type IndexErrMap map[string]error + +// MarshalJSON seralizes the error into a string for JSON consumption +func (iem IndexErrMap) MarshalJSON() ([]byte, error) { + tmp := make(map[string]string, len(iem)) + for k, v := range iem { + tmp[k] = v.Error() + } + return util.MarshalJSON(tmp) +} + +func (iem IndexErrMap) UnmarshalJSON(data []byte) error { + var tmp map[string]string + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + for k, v := range tmp { + iem[k] = fmt.Errorf("%s", v) + } + return nil +} + +// SearchStatus is a secion in the SearchResult reporting how many +// underlying indexes were queried, how many were successful/failed +// and a map of any errors that were encountered +type SearchStatus struct { + Total int `json:"total"` + Failed int `json:"failed"` + Successful int `json:"successful"` + Errors IndexErrMap `json:"errors,omitempty"` +} + +// Merge will merge together multiple SearchStatuses during a MultiSearch +func (ss *SearchStatus) Merge(other *SearchStatus) { + ss.Total += other.Total + ss.Failed += other.Failed + ss.Successful += other.Successful + if len(other.Errors) > 0 { + if ss.Errors == nil { + ss.Errors = make(map[string]error) + } + for otherIndex, otherError := range other.Errors { + ss.Errors[otherIndex] = otherError + } + } +} + +// A SearchResult describes the results of executing +// a SearchRequest. +// +// Status - Whether the search was executed on the underlying indexes successfully +// or failed, and the corresponding errors. +// Request - The SearchRequest that was executed. +// Hits - The list of documents that matched the query and their corresponding +// scores, score explanation, location info and so on. +// Total - The total number of documents that matched the query. +// Cost - indicates how expensive was the query with respect to bytes read +// from the mmaped index files. +// MaxScore - The maximum score seen across all document hits seen for this query. +// Took - The time taken to execute the search. +// Facets - The facet results for the search. +type SearchResult struct { + Status *SearchStatus `json:"status"` + Request *SearchRequest `json:"request,omitempty"` + Hits search.DocumentMatchCollection `json:"hits"` + Total uint64 `json:"total_hits"` + Cost uint64 `json:"cost"` + MaxScore float64 `json:"max_score"` + Took time.Duration `json:"took"` + Facets search.FacetResults `json:"facets"` + // special fields that are applicable only for search + // results that are obtained from a presearch + SynonymResult search.FieldTermSynonymMap `json:"synonym_result,omitempty"` + + // The following fields are applicable to BM25 preSearch + BM25Stats *search.BM25Stats `json:"bm25_stats,omitempty"` +} + +func (sr *SearchResult) Size() int { + sizeInBytes := reflectStaticSizeSearchResult + size.SizeOfPtr + + reflectStaticSizeSearchStatus + + for _, entry := range sr.Hits { + if entry != nil { + sizeInBytes += entry.Size() + } + } + + for k, v := range sr.Facets { + sizeInBytes += size.SizeOfString + len(k) + + v.Size() + } + + return sizeInBytes +} + +func (sr *SearchResult) String() string { + rv := "" + if sr.Total > 0 { + if sr.Request != nil && sr.Request.Size > 0 { + rv = fmt.Sprintf("%d matches, showing %d through %d, took %s\n", sr.Total, sr.Request.From+1, sr.Request.From+len(sr.Hits), sr.Took) + for i, hit := range sr.Hits { + rv += fmt.Sprintf("%5d. %s (%f)\n", i+sr.Request.From+1, hit.ID, hit.Score) + for fragmentField, fragments := range hit.Fragments { + rv += fmt.Sprintf("\t%s\n", fragmentField) + for _, fragment := range fragments { + rv += fmt.Sprintf("\t\t%s\n", fragment) + } + } + for otherFieldName, otherFieldValue := range hit.Fields { + if _, ok := hit.Fragments[otherFieldName]; !ok { + rv += fmt.Sprintf("\t%s\n", otherFieldName) + rv += fmt.Sprintf("\t\t%v\n", otherFieldValue) + } + } + } + } else { + rv = fmt.Sprintf("%d matches, took %s\n", sr.Total, sr.Took) + } + } else { + rv = "No matches" + } + if len(sr.Facets) > 0 { + rv += "Facets:\n" + for fn, f := range sr.Facets { + rv += fmt.Sprintf("%s(%d)\n", fn, f.Total) + for _, t := range f.Terms.Terms() { + rv += fmt.Sprintf("\t%s(%d)\n", t.Term, t.Count) + } + for _, n := range f.NumericRanges { + rv += fmt.Sprintf("\t%s(%d)\n", n.Name, n.Count) + } + for _, d := range f.DateRanges { + rv += fmt.Sprintf("\t%s(%d)\n", d.Name, d.Count) + } + if f.Other != 0 { + rv += fmt.Sprintf("\tOther(%d)\n", f.Other) + } + } + } + return rv +} + +// Merge will merge together multiple SearchResults during a MultiSearch +func (sr *SearchResult) Merge(other *SearchResult) { + sr.Status.Merge(other.Status) + sr.Hits = append(sr.Hits, other.Hits...) + sr.Total += other.Total + sr.Cost += other.Cost + if other.MaxScore > sr.MaxScore { + sr.MaxScore = other.MaxScore + } + if sr.Facets == nil && len(other.Facets) != 0 { + sr.Facets = other.Facets + return + } + + sr.Facets.Merge(other.Facets) +} + +// MemoryNeededForSearchResult is an exported helper function to determine the RAM +// needed to accommodate the results for a given search request. +func MemoryNeededForSearchResult(req *SearchRequest) uint64 { + if req == nil { + return 0 + } + + numDocMatches := req.Size + req.From + if req.Size+req.From > collector.PreAllocSizeSkipCap { + numDocMatches = collector.PreAllocSizeSkipCap + } + + estimate := 0 + + // overhead from the SearchResult structure + var sr SearchResult + estimate += sr.Size() + + var dm search.DocumentMatch + sizeOfDocumentMatch := dm.Size() + + // overhead from results + estimate += numDocMatches * sizeOfDocumentMatch + + // overhead from facet results + if req.Facets != nil { + var fr search.FacetResult + estimate += len(req.Facets) * fr.Size() + } + + // overhead from fields, highlighting + var d document.Document + if len(req.Fields) > 0 || req.Highlight != nil { + numDocsApplicable := req.Size + if numDocsApplicable > collector.PreAllocSizeSkipCap { + numDocsApplicable = collector.PreAllocSizeSkipCap + } + estimate += numDocsApplicable * d.Size() + } + + return uint64(estimate) +} + +// SetSortFunc sets the sort implementation to use when sorting hits. +// +// SearchRequests can specify a custom sort implementation to meet +// their needs. For instance, by specifying a parallel sort +// that uses all available cores. +func (r *SearchRequest) SetSortFunc(s func(sort.Interface)) { + r.sortFunc = s +} + +// SortFunc returns the sort implementation to use when sorting hits. +// Defaults to sort.Sort. +func (r *SearchRequest) SortFunc() func(data sort.Interface) { + if r.sortFunc != nil { + return r.sortFunc + } + + return sort.Sort +} + +func isMatchNoneQuery(q query.Query) bool { + _, ok := q.(*query.MatchNoneQuery) + return ok +} + +func isMatchAllQuery(q query.Query) bool { + _, ok := q.(*query.MatchAllQuery) + return ok +} diff --git a/search/collector.go b/search/collector.go new file mode 100644 index 0000000..e81219e --- /dev/null +++ b/search/collector.go @@ -0,0 +1,58 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "context" + "time" + + index "github.com/blevesearch/bleve_index_api" +) + +type Collector interface { + Collect(ctx context.Context, searcher Searcher, reader index.IndexReader) error + Results() DocumentMatchCollection + Total() uint64 + MaxScore() float64 + Took() time.Duration + SetFacetsBuilder(facetsBuilder *FacetsBuilder) + FacetResults() FacetResults +} + +// DocumentMatchHandler is the type of document match callback +// bleve will invoke during the search. +// Eventually, bleve will indicate the completion of an ongoing search, +// by passing a nil value for the document match callback. +// The application should take a copy of the hit/documentMatch +// if it wish to own it or need prolonged access to it. +type DocumentMatchHandler func(hit *DocumentMatch) error + +type MakeDocumentMatchHandlerKeyType string + +var MakeDocumentMatchHandlerKey = MakeDocumentMatchHandlerKeyType( + "MakeDocumentMatchHandlerKey") + +var MakeKNNDocumentMatchHandlerKey = MakeDocumentMatchHandlerKeyType( + "MakeKNNDocumentMatchHandlerKey") + +// MakeDocumentMatchHandler is an optional DocumentMatchHandler +// builder function which the applications can pass to bleve. +// These builder methods gives a DocumentMatchHandler function +// to bleve, which it will invoke on every document matches. +type MakeDocumentMatchHandler func(ctx *SearchContext) ( + callback DocumentMatchHandler, loadID bool, err error) + +type MakeKNNDocumentMatchHandler func(ctx *SearchContext) ( + callback DocumentMatchHandler, err error) diff --git a/search/collector/bench_test.go b/search/collector/bench_test.go new file mode 100644 index 0000000..3edf369 --- /dev/null +++ b/search/collector/bench_test.go @@ -0,0 +1,50 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "math/rand" + "strconv" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +type createCollector func() search.Collector + +func benchHelper(numOfMatches int, cc createCollector, b *testing.B) { + matches := make([]*search.DocumentMatch, 0, numOfMatches) + for i := 0; i < numOfMatches; i++ { + matches = append(matches, &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID(strconv.Itoa(i)), + Score: rand.Float64(), + }) + } + + b.ResetTimer() + + for run := 0; run < b.N; run++ { + searcher := &stubSearcher{ + matches: matches, + } + collector := cc() + err := collector.Collect(context.Background(), searcher, &stubReader{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/search/collector/eligible.go b/search/collector/eligible.go new file mode 100644 index 0000000..49e0448 --- /dev/null +++ b/search/collector/eligible.go @@ -0,0 +1,172 @@ +// Copyright (c) 2024 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package collector + +import ( + "context" + "fmt" + "time" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +type EligibleCollector struct { + size int + total uint64 + took time.Duration + eligibleSelector index.EligibleDocumentSelector +} + +func NewEligibleCollector(size int) *EligibleCollector { + return newEligibleCollector(size) +} + +func newEligibleCollector(size int) *EligibleCollector { + // No sort order & skip always 0 since this is only to filter eligible docs. + ec := &EligibleCollector{ + size: size, + } + return ec +} + +func makeEligibleDocumentMatchHandler(ctx *search.SearchContext, reader index.IndexReader) (search.DocumentMatchHandler, error) { + if ec, ok := ctx.Collector.(*EligibleCollector); ok { + if vr, ok := reader.(index.VectorIndexReader); ok { + // create a new eligible document selector to add eligible document matches + ec.eligibleSelector = vr.NewEligibleDocumentSelector() + // return a document match handler that adds eligible document matches + // to the eligible document selector + return func(d *search.DocumentMatch) error { + if d == nil { + return nil + } + err := ec.eligibleSelector.AddEligibleDocumentMatch(d.IndexInternalID) + if err != nil { + return err + } + // recycle the DocumentMatch + ctx.DocumentMatchPool.Put(d) + return nil + }, nil + } + return nil, fmt.Errorf("reader is not a VectorIndexReader") + } + + return nil, fmt.Errorf("eligiblity collector not available") +} + +func (ec *EligibleCollector) Collect(ctx context.Context, searcher search.Searcher, reader index.IndexReader) error { + startTime := time.Now() + var err error + var next *search.DocumentMatch + + backingSize := ec.size + if backingSize > PreAllocSizeSkipCap { + backingSize = PreAllocSizeSkipCap + 1 + } + searchContext := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(backingSize+searcher.DocumentMatchPoolSize(), 0), + Collector: ec, + IndexReader: reader, + } + + dmHandler, err := makeEligibleDocumentMatchHandler(searchContext, reader) + if err != nil { + return err + } + select { + case <-ctx.Done(): + search.RecordSearchCost(ctx, search.AbortM, 0) + return ctx.Err() + default: + next, err = searcher.Next(searchContext) + } + for err == nil && next != nil { + if ec.total%CheckDoneEvery == 0 { + select { + case <-ctx.Done(): + search.RecordSearchCost(ctx, search.AbortM, 0) + return ctx.Err() + default: + } + } + ec.total++ + + err = dmHandler(next) + if err != nil { + break + } + + next, err = searcher.Next(searchContext) + } + if err != nil { + return err + } + + // help finalize/flush the results in case + // of custom document match handlers. + err = dmHandler(nil) + if err != nil { + return err + } + + // compute search duration + ec.took = time.Since(startTime) + + return nil +} + +// The eligible collector does not return any document matches and hence +// this method is a dummy method returning nil, to conform to the +// search.Collector interface. +func (ec *EligibleCollector) Results() search.DocumentMatchCollection { + return nil +} + +// EligibleSelector returns the eligible document selector, which can be used +// to retrieve the list of eligible documents from this collector. +// If the collector has no results, it returns nil. +func (ec *EligibleCollector) EligibleSelector() index.EligibleDocumentSelector { + if ec.total == 0 { + return nil + } + return ec.eligibleSelector +} + +func (ec *EligibleCollector) Total() uint64 { + return ec.total +} + +// No concept of scoring in the eligible collector. +func (ec *EligibleCollector) MaxScore() float64 { + return 0 +} + +func (ec *EligibleCollector) Took() time.Duration { + return ec.took +} + +func (ec *EligibleCollector) SetFacetsBuilder(facetsBuilder *search.FacetsBuilder) { + // facet unsupported for pre-filtering in KNN search +} + +func (ec *EligibleCollector) FacetResults() search.FacetResults { + // facet unsupported for pre-filtering in KNN search + return nil +} diff --git a/search/collector/heap.go b/search/collector/heap.go new file mode 100644 index 0000000..cd662bc --- /dev/null +++ b/search/collector/heap.go @@ -0,0 +1,99 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "container/heap" + + "github.com/blevesearch/bleve/v2/search" +) + +type collectStoreHeap struct { + heap search.DocumentMatchCollection + compare collectorCompare +} + +func newStoreHeap(capacity int, compare collectorCompare) *collectStoreHeap { + rv := &collectStoreHeap{ + heap: make(search.DocumentMatchCollection, 0, capacity), + compare: compare, + } + heap.Init(rv) + return rv +} + +func (c *collectStoreHeap) AddNotExceedingSize(doc *search.DocumentMatch, + size int) *search.DocumentMatch { + c.add(doc) + if c.Len() > size { + return c.removeLast() + } + return nil +} + +func (c *collectStoreHeap) add(doc *search.DocumentMatch) { + heap.Push(c, doc) +} + +func (c *collectStoreHeap) removeLast() *search.DocumentMatch { + return heap.Pop(c).(*search.DocumentMatch) +} + +func (c *collectStoreHeap) Final(skip int, fixup collectorFixup) (search.DocumentMatchCollection, error) { + count := c.Len() + size := count - skip + if size <= 0 { + return make(search.DocumentMatchCollection, 0), nil + } + rv := make(search.DocumentMatchCollection, size) + for i := size - 1; i >= 0; i-- { + doc := heap.Pop(c).(*search.DocumentMatch) + rv[i] = doc + err := fixup(doc) + if err != nil { + return nil, err + } + } + return rv, nil +} + +func (c *collectStoreHeap) Internal() search.DocumentMatchCollection { + return c.heap +} + +// heap interface implementation + +func (c *collectStoreHeap) Len() int { + return len(c.heap) +} + +func (c *collectStoreHeap) Less(i, j int) bool { + so := c.compare(c.heap[i], c.heap[j]) + return -so < 0 +} + +func (c *collectStoreHeap) Swap(i, j int) { + c.heap[i], c.heap[j] = c.heap[j], c.heap[i] +} + +func (c *collectStoreHeap) Push(x interface{}) { + c.heap = append(c.heap, x.(*search.DocumentMatch)) +} + +func (c *collectStoreHeap) Pop() interface{} { + var rv *search.DocumentMatch + rv, c.heap = c.heap[len(c.heap)-1], c.heap[:len(c.heap)-1] + return rv +} diff --git a/search/collector/knn.go b/search/collector/knn.go new file mode 100644 index 0000000..465bf69 --- /dev/null +++ b/search/collector/knn.go @@ -0,0 +1,262 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package collector + +import ( + "context" + "time" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +type collectStoreKNN struct { + internalHeaps []collectorStore + kValues []int64 + allHits map[*search.DocumentMatch]struct{} + ejectedDocs map[*search.DocumentMatch]struct{} +} + +func newStoreKNN(internalHeaps []collectorStore, kValues []int64) *collectStoreKNN { + return &collectStoreKNN{ + internalHeaps: internalHeaps, + kValues: kValues, + ejectedDocs: make(map[*search.DocumentMatch]struct{}), + allHits: make(map[*search.DocumentMatch]struct{}), + } +} + +// Adds a document to the collector store and returns the documents that were ejected +// from the store. The documents that were ejected from the store are the ones that +// were not in the top K documents for any of the heaps. +// These document are put back into the pool document match pool in the KNN Collector. +func (c *collectStoreKNN) AddDocument(doc *search.DocumentMatch) []*search.DocumentMatch { + for heapIdx := 0; heapIdx < len(c.internalHeaps); heapIdx++ { + if _, ok := doc.ScoreBreakdown[heapIdx]; !ok { + continue + } + ejectedDoc := c.internalHeaps[heapIdx].AddNotExceedingSize(doc, int(c.kValues[heapIdx])) + if ejectedDoc != nil { + delete(ejectedDoc.ScoreBreakdown, heapIdx) + c.ejectedDocs[ejectedDoc] = struct{}{} + } + } + var rv []*search.DocumentMatch + for doc := range c.ejectedDocs { + if len(doc.ScoreBreakdown) == 0 { + rv = append(rv, doc) + } + // clear out the ejectedDocs map to reuse it in the next AddDocument call + delete(c.ejectedDocs, doc) + } + return rv +} + +func (c *collectStoreKNN) Final(fixup collectorFixup) (search.DocumentMatchCollection, error) { + for _, heap := range c.internalHeaps { + for _, doc := range heap.Internal() { + // duplicates may be present across the internal heaps + // meaning the same document match may be in the top K + // for multiple KNN queries. + c.allHits[doc] = struct{}{} + } + } + size := len(c.allHits) + if size <= 0 { + return make(search.DocumentMatchCollection, 0), nil + } + rv := make(search.DocumentMatchCollection, size) + i := 0 + for doc := range c.allHits { + if fixup != nil { + err := fixup(doc) + if err != nil { + return nil, err + } + } + rv[i] = doc + i++ + } + return rv, nil +} + +func MakeKNNDocMatchHandler(ctx *search.SearchContext) (search.DocumentMatchHandler, error) { + var hc *KNNCollector + var ok bool + if hc, ok = ctx.Collector.(*KNNCollector); ok { + return func(d *search.DocumentMatch) error { + if d == nil { + return nil + } + toRelease := hc.knnStore.AddDocument(d) + for _, doc := range toRelease { + ctx.DocumentMatchPool.Put(doc) + } + return nil + }, nil + } + return nil, nil +} + +func GetNewKNNCollectorStore(kArray []int64) *collectStoreKNN { + internalHeaps := make([]collectorStore, len(kArray)) + for knnIdx, k := range kArray { + // TODO - Check if the datatype of k can be made into an int instead of int64 + idx := knnIdx + internalHeaps[idx] = getOptimalCollectorStore(int(k), 0, func(i, j *search.DocumentMatch) int { + if i.ScoreBreakdown[idx] < j.ScoreBreakdown[idx] { + return 1 + } + return -1 + }) + } + return newStoreKNN(internalHeaps, kArray) +} + +// implements Collector interface +type KNNCollector struct { + knnStore *collectStoreKNN + size int + total uint64 + took time.Duration + results search.DocumentMatchCollection + maxScore float64 +} + +func NewKNNCollector(kArray []int64, size int64) *KNNCollector { + return &KNNCollector{ + knnStore: GetNewKNNCollectorStore(kArray), + size: int(size), + } +} + +func (hc *KNNCollector) Collect(ctx context.Context, searcher search.Searcher, reader index.IndexReader) error { + startTime := time.Now() + var err error + var next *search.DocumentMatch + + // pre-allocate enough space in the DocumentMatchPool + // unless the sum of K is too large, then cap it + // everything should still work, just allocates DocumentMatches on demand + backingSize := hc.size + if backingSize > PreAllocSizeSkipCap { + backingSize = PreAllocSizeSkipCap + 1 + } + searchContext := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(backingSize+searcher.DocumentMatchPoolSize(), 0), + Collector: hc, + IndexReader: reader, + } + + dmHandlerMakerKNN := MakeKNNDocMatchHandler + if cv := ctx.Value(search.MakeKNNDocumentMatchHandlerKey); cv != nil { + dmHandlerMakerKNN = cv.(search.MakeKNNDocumentMatchHandler) + } + // use the application given builder for making the custom document match + // handler and perform callbacks/invocations on the newly made handler. + dmHandler, err := dmHandlerMakerKNN(searchContext) + if err != nil { + return err + } + select { + case <-ctx.Done(): + search.RecordSearchCost(ctx, search.AbortM, 0) + return ctx.Err() + default: + next, err = searcher.Next(searchContext) + } + for err == nil && next != nil { + if hc.total%CheckDoneEvery == 0 { + select { + case <-ctx.Done(): + search.RecordSearchCost(ctx, search.AbortM, 0) + return ctx.Err() + default: + } + } + hc.total++ + + err = dmHandler(next) + if err != nil { + break + } + + next, err = searcher.Next(searchContext) + } + if err != nil { + return err + } + + // help finalize/flush the results in case + // of custom document match handlers. + err = dmHandler(nil) + if err != nil { + return err + } + + // compute search duration + hc.took = time.Since(startTime) + + // finalize actual results + err = hc.finalizeResults(reader) + if err != nil { + return err + } + return nil +} + +func (hc *KNNCollector) finalizeResults(r index.IndexReader) error { + var err error + hc.results, err = hc.knnStore.Final(func(doc *search.DocumentMatch) error { + if doc.ID == "" { + // look up the id since we need it for lookup + var err error + doc.ID, err = r.ExternalID(doc.IndexInternalID) + if err != nil { + return err + } + } + return nil + }) + return err +} + +func (hc *KNNCollector) Results() search.DocumentMatchCollection { + return hc.results +} + +func (hc *KNNCollector) Total() uint64 { + return hc.total +} + +func (hc *KNNCollector) MaxScore() float64 { + return hc.maxScore +} + +func (hc *KNNCollector) Took() time.Duration { + return hc.took +} + +func (hc *KNNCollector) SetFacetsBuilder(facetsBuilder *search.FacetsBuilder) { + // facet unsupported for vector search +} + +func (hc *KNNCollector) FacetResults() search.FacetResults { + // facet unsupported for vector search + return nil +} diff --git a/search/collector/list.go b/search/collector/list.go new file mode 100644 index 0000000..f73505e --- /dev/null +++ b/search/collector/list.go @@ -0,0 +1,96 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "container/list" + + "github.com/blevesearch/bleve/v2/search" +) + +type collectStoreList struct { + results *list.List + compare collectorCompare +} + +func newStoreList(capacity int, compare collectorCompare) *collectStoreList { + rv := &collectStoreList{ + results: list.New(), + compare: compare, + } + + return rv +} + +func (c *collectStoreList) AddNotExceedingSize(doc *search.DocumentMatch, size int) *search.DocumentMatch { + c.add(doc) + if c.len() > size { + return c.removeLast() + } + return nil +} + +func (c *collectStoreList) add(doc *search.DocumentMatch) { + for e := c.results.Front(); e != nil; e = e.Next() { + curr := e.Value.(*search.DocumentMatch) + if c.compare(doc, curr) >= 0 { + c.results.InsertBefore(doc, e) + return + } + } + // if we got to the end, we still have to add it + c.results.PushBack(doc) +} + +func (c *collectStoreList) removeLast() *search.DocumentMatch { + return c.results.Remove(c.results.Front()).(*search.DocumentMatch) +} + +func (c *collectStoreList) Final(skip int, fixup collectorFixup) (search.DocumentMatchCollection, error) { + if c.results.Len()-skip > 0 { + rv := make(search.DocumentMatchCollection, c.results.Len()-skip) + i := 0 + skipped := 0 + for e := c.results.Back(); e != nil; e = e.Prev() { + if skipped < skip { + skipped++ + continue + } + + rv[i] = e.Value.(*search.DocumentMatch) + err := fixup(rv[i]) + if err != nil { + return nil, err + } + i++ + } + return rv, nil + } + return search.DocumentMatchCollection{}, nil +} + +func (c *collectStoreList) Internal() search.DocumentMatchCollection { + rv := make(search.DocumentMatchCollection, c.results.Len()) + i := 0 + for e := c.results.Front(); e != nil; e = e.Next() { + rv[i] = e.Value.(*search.DocumentMatch) + i++ + } + return rv +} + +func (c *collectStoreList) len() int { + return c.results.Len() +} diff --git a/search/collector/search_test.go b/search/collector/search_test.go new file mode 100644 index 0000000..1f6f882 --- /dev/null +++ b/search/collector/search_test.go @@ -0,0 +1,187 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +type stubSearcher struct { + index int + matches []*search.DocumentMatch +} + +func (ss *stubSearcher) SetBytesRead(val uint64) { + +} + +func (ss *stubSearcher) BytesRead() uint64 { + return 0 +} + +func (ss *stubSearcher) Size() int { + sizeInBytes := int(reflect.TypeOf(*ss).Size()) + + for _, entry := range ss.matches { + if entry != nil { + sizeInBytes += entry.Size() + } + } + + return sizeInBytes +} + +func (ss *stubSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + if ss.index < len(ss.matches) { + rv := ctx.DocumentMatchPool.Get() + rv.IndexInternalID = ss.matches[ss.index].IndexInternalID + rv.Score = ss.matches[ss.index].Score + ss.index++ + return rv, nil + } + return nil, nil +} + +func (ss *stubSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + + for ss.index < len(ss.matches) && ss.matches[ss.index].IndexInternalID.Compare(ID) < 0 { + ss.index++ + } + if ss.index < len(ss.matches) { + rv := ctx.DocumentMatchPool.Get() + rv.IndexInternalID = ss.matches[ss.index].IndexInternalID + rv.Score = ss.matches[ss.index].Score + ss.index++ + return rv, nil + } + return nil, nil +} + +func (ss *stubSearcher) Close() error { + return nil +} + +func (ss *stubSearcher) Weight() float64 { + return 0.0 +} + +func (ss *stubSearcher) SetQueryNorm(float64) { +} + +func (ss *stubSearcher) Count() uint64 { + return uint64(len(ss.matches)) +} + +func (ss *stubSearcher) Min() int { + return 0 +} + +func (ss *stubSearcher) DocumentMatchPoolSize() int { + return 0 +} + +type stubReader struct{} + +func (sr *stubReader) Size() int { + return 0 +} + +func (sr *stubReader) TermFieldReader(ctx context.Context, term []byte, field string, includeFreq, includeNorm, includeTermVectors bool) (index.TermFieldReader, error) { + return nil, nil +} + +func (sr *stubReader) DocIDReaderAll() (index.DocIDReader, error) { + return nil, nil +} + +func (sr *stubReader) DocIDReaderOnly(ids []string) (index.DocIDReader, error) { + return nil, nil +} + +func (sr *stubReader) FieldDict(field string) (index.FieldDict, error) { + return nil, nil +} + +func (sr *stubReader) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) { + return nil, nil +} + +func (sr *stubReader) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) { + return nil, nil +} + +func (sr *stubReader) Document(id string) (index.Document, error) { + return nil, nil +} + +func (sr *stubReader) DocumentVisitFieldTerms(id index.IndexInternalID, fields []string, visitor index.DocValueVisitor) error { + return nil +} + +func (sr *stubReader) Fields() ([]string, error) { + return nil, nil +} + +func (sr *stubReader) GetInternal(key []byte) ([]byte, error) { + return nil, nil +} + +func (sr *stubReader) DocCount() (uint64, error) { + return 0, nil +} + +func (sr *stubReader) ExternalID(id index.IndexInternalID) (string, error) { + return string(id), nil +} + +func (sr *stubReader) InternalID(id string) (index.IndexInternalID, error) { + return []byte(id), nil +} + +func (sr *stubReader) DumpAll() chan interface{} { + return nil +} + +func (sr *stubReader) DumpDoc(id string) chan interface{} { + return nil +} + +func (sr *stubReader) DumpFields() chan interface{} { + return nil +} + +func (sr *stubReader) Close() error { + return nil +} + +func (sr *stubReader) DocValueReader(fields []string) (index.DocValueReader, error) { + return &DocValueReader{i: sr, fields: fields}, nil +} + +type DocValueReader struct { + i *stubReader + fields []string +} + +func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID, visitor index.DocValueVisitor) error { + return dvr.i.DocumentVisitFieldTerms(id, dvr.fields, visitor) +} +func (dvr *DocValueReader) BytesRead() uint64 { + return 0 +} diff --git a/search/collector/slice.go b/search/collector/slice.go new file mode 100644 index 0000000..6120921 --- /dev/null +++ b/search/collector/slice.go @@ -0,0 +1,83 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "github.com/blevesearch/bleve/v2/search" +) + +type collectStoreSlice struct { + slice search.DocumentMatchCollection + compare collectorCompare +} + +func newStoreSlice(capacity int, compare collectorCompare) *collectStoreSlice { + rv := &collectStoreSlice{ + slice: make(search.DocumentMatchCollection, 0, capacity), + compare: compare, + } + return rv +} + +func (c *collectStoreSlice) AddNotExceedingSize(doc *search.DocumentMatch, + size int) *search.DocumentMatch { + c.add(doc) + if c.len() > size { + return c.removeLast() + } + return nil +} + +func (c *collectStoreSlice) add(doc *search.DocumentMatch) { + // find where to insert, starting at end (lowest) + i := len(c.slice) + for ; i > 0; i-- { + cmp := c.compare(doc, c.slice[i-1]) + if cmp >= 0 { + break + } + } + // insert at i + c.slice = append(c.slice, nil) + copy(c.slice[i+1:], c.slice[i:]) + c.slice[i] = doc +} + +func (c *collectStoreSlice) removeLast() *search.DocumentMatch { + var rv *search.DocumentMatch + rv, c.slice = c.slice[len(c.slice)-1], c.slice[:len(c.slice)-1] + return rv +} + +func (c *collectStoreSlice) Final(skip int, fixup collectorFixup) (search.DocumentMatchCollection, error) { + for i := skip; i < len(c.slice); i++ { + err := fixup(c.slice[i]) + if err != nil { + return nil, err + } + } + if skip <= len(c.slice) { + return c.slice[skip:], nil + } + return search.DocumentMatchCollection{}, nil +} + +func (c *collectStoreSlice) Internal() search.DocumentMatchCollection { + return c.slice +} + +func (c *collectStoreSlice) len() int { + return len(c.slice) +} diff --git a/search/collector/topn.go b/search/collector/topn.go new file mode 100644 index 0000000..e3ea9d7 --- /dev/null +++ b/search/collector/topn.go @@ -0,0 +1,558 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + "reflect" + "strconv" + "time" + + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeTopNCollector int + +func init() { + var coll TopNCollector + reflectStaticSizeTopNCollector = int(reflect.TypeOf(coll).Size()) +} + +type collectorStore interface { + // Add the document, and if the new store size exceeds the provided size + // the last element is removed and returned. If the size has not been + // exceeded, nil is returned. + AddNotExceedingSize(doc *search.DocumentMatch, size int) *search.DocumentMatch + + Final(skip int, fixup collectorFixup) (search.DocumentMatchCollection, error) + + // Provide access the internal heap implementation + Internal() search.DocumentMatchCollection +} + +// PreAllocSizeSkipCap will cap preallocation to this amount when +// size+skip exceeds this value +var PreAllocSizeSkipCap = 1000 + +type collectorCompare func(i, j *search.DocumentMatch) int + +type collectorFixup func(d *search.DocumentMatch) error + +// TopNCollector collects the top N hits, optionally skipping some results +type TopNCollector struct { + size int + skip int + total uint64 + bytesRead uint64 + maxScore float64 + took time.Duration + sort search.SortOrder + results search.DocumentMatchCollection + facetsBuilder *search.FacetsBuilder + + store collectorStore + + needDocIds bool + neededFields []string + cachedScoring []bool + cachedDesc []bool + + lowestMatchOutsideResults *search.DocumentMatch + updateFieldVisitor index.DocValueVisitor + dvReader index.DocValueReader + searchAfter *search.DocumentMatch + + knnHits map[string]*search.DocumentMatch + computeNewScoreExpl search.ScoreExplCorrectionCallbackFunc +} + +// CheckDoneEvery controls how frequently we check the context deadline +const CheckDoneEvery = uint64(1024) + +// NewTopNCollector builds a collector to find the top 'size' hits +// skipping over the first 'skip' hits +// ordering hits by the provided sort order +func NewTopNCollector(size int, skip int, sort search.SortOrder) *TopNCollector { + return newTopNCollector(size, skip, sort) +} + +// NewTopNCollectorAfter builds a collector to find the top 'size' hits +// skipping over the first 'skip' hits +// ordering hits by the provided sort order +func NewTopNCollectorAfter(size int, sort search.SortOrder, after []string) *TopNCollector { + rv := newTopNCollector(size, 0, sort) + rv.searchAfter = createSearchAfterDocument(sort, after) + return rv +} + +func newTopNCollector(size int, skip int, sort search.SortOrder) *TopNCollector { + hc := &TopNCollector{size: size, skip: skip, sort: sort} + + hc.store = getOptimalCollectorStore(size, skip, func(i, j *search.DocumentMatch) int { + return hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, i, j) + }) + + // these lookups traverse an interface, so do once up-front + if sort.RequiresDocID() { + hc.needDocIds = true + } + hc.neededFields = sort.RequiredFields() + hc.cachedScoring = sort.CacheIsScore() + hc.cachedDesc = sort.CacheDescending() + + return hc +} + +func createSearchAfterDocument(sort search.SortOrder, after []string) *search.DocumentMatch { + rv := &search.DocumentMatch{ + Sort: after, + } + for pos, ss := range sort { + if ss.RequiresDocID() { + rv.ID = after[pos] + } + if ss.RequiresScoring() { + if score, err := strconv.ParseFloat(after[pos], 64); err == nil { + rv.Score = score + } + } + } + return rv +} + +// Filter document matches based on the SearchAfter field in the SearchRequest. +func FilterHitsBySearchAfter(hits []*search.DocumentMatch, sort search.SortOrder, after []string) []*search.DocumentMatch { + if len(hits) == 0 { + return hits + } + // create a search after document + searchAfter := createSearchAfterDocument(sort, after) + // filter the hits + idx := 0 + cachedScoring := sort.CacheIsScore() + cachedDesc := sort.CacheDescending() + for _, hit := range hits { + if sort.Compare(cachedScoring, cachedDesc, hit, searchAfter) > 0 { + hits[idx] = hit + idx++ + } + } + return hits[:idx] +} + +func getOptimalCollectorStore(size, skip int, comparator collectorCompare) collectorStore { + // pre-allocate space on the store to avoid reslicing + // unless the size + skip is too large, then cap it + // everything should still work, just reslices as necessary + backingSize := size + skip + 1 + if size+skip > PreAllocSizeSkipCap { + backingSize = PreAllocSizeSkipCap + 1 + } + + if size+skip > 10 { + return newStoreHeap(backingSize, comparator) + } else { + return newStoreSlice(backingSize, comparator) + } +} + +func (hc *TopNCollector) Size() int { + sizeInBytes := reflectStaticSizeTopNCollector + size.SizeOfPtr + + if hc.facetsBuilder != nil { + sizeInBytes += hc.facetsBuilder.Size() + } + + for _, entry := range hc.neededFields { + sizeInBytes += len(entry) + size.SizeOfString + } + + sizeInBytes += len(hc.cachedScoring) + len(hc.cachedDesc) + + return sizeInBytes +} + +// Collect goes to the index to find the matching documents +func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher, reader index.IndexReader) error { + startTime := time.Now() + var err error + var next *search.DocumentMatch + + // pre-allocate enough space in the DocumentMatchPool + // unless the size + skip is too large, then cap it + // everything should still work, just allocates DocumentMatches on demand + backingSize := hc.size + hc.skip + 1 + if hc.size+hc.skip > PreAllocSizeSkipCap { + backingSize = PreAllocSizeSkipCap + 1 + } + searchContext := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(backingSize+searcher.DocumentMatchPoolSize(), len(hc.sort)), + Collector: hc, + IndexReader: reader, + } + + hc.dvReader, err = reader.DocValueReader(hc.neededFields) + if err != nil { + return err + } + + hc.updateFieldVisitor = func(field string, term []byte) { + if hc.facetsBuilder != nil { + hc.facetsBuilder.UpdateVisitor(field, term) + } + hc.sort.UpdateVisitor(field, term) + } + + dmHandlerMaker := MakeTopNDocumentMatchHandler + if cv := ctx.Value(search.MakeDocumentMatchHandlerKey); cv != nil { + dmHandlerMaker = cv.(search.MakeDocumentMatchHandler) + } + // use the application given builder for making the custom document match + // handler and perform callbacks/invocations on the newly made handler. + dmHandler, loadID, err := dmHandlerMaker(searchContext) + if err != nil { + return err + } + + hc.needDocIds = hc.needDocIds || loadID + select { + case <-ctx.Done(): + search.RecordSearchCost(ctx, search.AbortM, 0) + return ctx.Err() + default: + next, err = searcher.Next(searchContext) + } + for err == nil && next != nil { + if hc.total%CheckDoneEvery == 0 { + select { + case <-ctx.Done(): + search.RecordSearchCost(ctx, search.AbortM, 0) + return ctx.Err() + default: + } + } + + err = hc.adjustDocumentMatch(searchContext, reader, next) + if err != nil { + break + } + + err = hc.prepareDocumentMatch(searchContext, reader, next, false) + if err != nil { + break + } + + err = dmHandler(next) + if err != nil { + break + } + + next, err = searcher.Next(searchContext) + } + if err != nil { + return err + } + if hc.knnHits != nil { + // we may have some knn hits left that did not match any of the top N tf-idf hits + // we need to add them to the collector store to consider them as well. + for _, knnDoc := range hc.knnHits { + err = hc.prepareDocumentMatch(searchContext, reader, knnDoc, true) + if err != nil { + return err + } + err = dmHandler(knnDoc) + if err != nil { + return err + } + } + } + + statsCallbackFn := ctx.Value(search.SearchIOStatsCallbackKey) + if statsCallbackFn != nil { + // hc.bytesRead corresponds to the + // total bytes read as part of docValues being read every hit + // which must be accounted by invoking the callback. + statsCallbackFn.(search.SearchIOStatsCallbackFunc)(hc.bytesRead) + + search.RecordSearchCost(ctx, search.AddM, hc.bytesRead) + } + + // help finalize/flush the results in case + // of custom document match handlers. + err = dmHandler(nil) + if err != nil { + return err + } + + // compute search duration + hc.took = time.Since(startTime) + + // finalize actual results + err = hc.finalizeResults(reader) + if err != nil { + return err + } + return nil +} + +var sortByScoreOpt = []string{"_score"} + +func (hc *TopNCollector) adjustDocumentMatch(ctx *search.SearchContext, + reader index.IndexReader, d *search.DocumentMatch) (err error) { + if hc.knnHits != nil { + d.ID, err = reader.ExternalID(d.IndexInternalID) + if err != nil { + return err + } + if knnHit, ok := hc.knnHits[d.ID]; ok { + d.Score, d.Expl = hc.computeNewScoreExpl(d, knnHit) + delete(hc.knnHits, d.ID) + } + } + return nil +} + +func (hc *TopNCollector) prepareDocumentMatch(ctx *search.SearchContext, + reader index.IndexReader, d *search.DocumentMatch, isKnnDoc bool) (err error) { + + // visit field terms for features that require it (sort, facets) + if !isKnnDoc && len(hc.neededFields) > 0 { + err = hc.visitFieldTerms(reader, d, hc.updateFieldVisitor) + if err != nil { + return err + } + } else if isKnnDoc && hc.facetsBuilder != nil { + // we need to visit the field terms for the knn document + // only for those fields that are required for faceting + // and not for sorting. This is because the knn document's + // sort value is already computed in the knn collector. + err = hc.visitFieldTerms(reader, d, func(field string, term []byte) { + if hc.facetsBuilder != nil { + hc.facetsBuilder.UpdateVisitor(field, term) + } + }) + if err != nil { + return err + } + } + + // increment total hits + hc.total++ + d.HitNumber = hc.total + + // update max score + if d.Score > hc.maxScore { + hc.maxScore = d.Score + } + // early exit as the document match had its sort value calculated in the knn + // collector itself + if isKnnDoc { + return nil + } + + // see if we need to load ID (at this early stage, for example to sort on it) + if hc.needDocIds && d.ID == "" { + d.ID, err = reader.ExternalID(d.IndexInternalID) + if err != nil { + return err + } + } + + // compute this hits sort value + if len(hc.sort) == 1 && hc.cachedScoring[0] { + d.Sort = sortByScoreOpt + } else { + hc.sort.Value(d) + } + + return nil +} + +func MakeTopNDocumentMatchHandler( + ctx *search.SearchContext) (search.DocumentMatchHandler, bool, error) { + var hc *TopNCollector + var ok bool + if hc, ok = ctx.Collector.(*TopNCollector); ok { + return func(d *search.DocumentMatch) error { + if d == nil { + return nil + } + + // support search after based pagination, + // if this hit is <= the search after sort key + // we should skip it + if hc.searchAfter != nil { + // exact sort order matches use hit number to break tie + // but we want to allow for exact match, so we pretend + hc.searchAfter.HitNumber = d.HitNumber + if hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, d, hc.searchAfter) <= 0 { + ctx.DocumentMatchPool.Put(d) + return nil + } + } + + // optimization, we track lowest sorting hit already removed from heap + // with this one comparison, we can avoid all heap operations if + // this hit would have been added and then immediately removed + if hc.lowestMatchOutsideResults != nil { + cmp := hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, d, + hc.lowestMatchOutsideResults) + if cmp >= 0 { + // this hit can't possibly be in the result set, so avoid heap ops + ctx.DocumentMatchPool.Put(d) + return nil + } + } + + removed := hc.store.AddNotExceedingSize(d, hc.size+hc.skip) + if removed != nil { + if hc.lowestMatchOutsideResults == nil { + hc.lowestMatchOutsideResults = removed + } else { + cmp := hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, + removed, hc.lowestMatchOutsideResults) + if cmp < 0 { + tmp := hc.lowestMatchOutsideResults + hc.lowestMatchOutsideResults = removed + ctx.DocumentMatchPool.Put(tmp) + } + } + } + return nil + }, false, nil + } + return nil, false, nil +} + +// visitFieldTerms is responsible for visiting the field terms of the +// search hit, and passing visited terms to the sort and facet builder +func (hc *TopNCollector) visitFieldTerms(reader index.IndexReader, d *search.DocumentMatch, v index.DocValueVisitor) error { + if hc.facetsBuilder != nil { + hc.facetsBuilder.StartDoc() + } + if d.ID != "" && d.IndexInternalID == nil { + // this document may have been sent over as preSearchData and + // we need to look up the internal id to visit the doc values for it + var err error + d.IndexInternalID, err = reader.InternalID(d.ID) + if err != nil { + return err + } + } + + err := hc.dvReader.VisitDocValues(d.IndexInternalID, v) + if hc.facetsBuilder != nil { + hc.facetsBuilder.EndDoc() + } + + hc.bytesRead += hc.dvReader.BytesRead() + + return err +} + +// SetFacetsBuilder registers a facet builder for this collector +func (hc *TopNCollector) SetFacetsBuilder(facetsBuilder *search.FacetsBuilder) { + hc.facetsBuilder = facetsBuilder + fieldsRequiredForFaceting := facetsBuilder.RequiredFields() + // for each of these fields, append only if not already there in hc.neededFields. + for _, field := range fieldsRequiredForFaceting { + found := false + for _, neededField := range hc.neededFields { + if field == neededField { + found = true + break + } + } + if !found { + hc.neededFields = append(hc.neededFields, field) + } + } +} + +// finalizeResults starts with the heap containing the final top size+skip +// it now throws away the results to be skipped +// and does final doc id lookup (if necessary) +func (hc *TopNCollector) finalizeResults(r index.IndexReader) error { + var err error + hc.results, err = hc.store.Final(hc.skip, func(doc *search.DocumentMatch) error { + if doc.ID == "" { + // look up the id since we need it for lookup + var err error + doc.ID, err = r.ExternalID(doc.IndexInternalID) + if err != nil { + return err + } + } + doc.Complete(nil) + return nil + }) + if err != nil { + return err + } + + // Decode geo sort keys back to its distance values + for i, so := range hc.sort { + if _, ok := so.(*search.SortGeoDistance); ok { + for _, dm := range hc.results { + // The string is a int64 bit representation of a float64 distance + distInt, err := numeric.PrefixCoded(dm.Sort[i]).Int64() + if err != nil { + return err + } + dm.Sort[i] = strconv.FormatFloat(numeric.Int64ToFloat64(distInt), 'f', -1, 64) + } + } + } + return err +} + +// Results returns the collected hits +func (hc *TopNCollector) Results() search.DocumentMatchCollection { + return hc.results +} + +// Total returns the total number of hits +func (hc *TopNCollector) Total() uint64 { + return hc.total +} + +// MaxScore returns the maximum score seen across all the hits +func (hc *TopNCollector) MaxScore() float64 { + return hc.maxScore +} + +// Took returns the time spent collecting hits +func (hc *TopNCollector) Took() time.Duration { + return hc.took +} + +// FacetResults returns the computed facets results +func (hc *TopNCollector) FacetResults() search.FacetResults { + if hc.facetsBuilder != nil { + return hc.facetsBuilder.Results() + } + return nil +} + +func (hc *TopNCollector) SetKNNHits(knnHits search.DocumentMatchCollection, newScoreExplComputer search.ScoreExplCorrectionCallbackFunc) { + hc.knnHits = make(map[string]*search.DocumentMatch, len(knnHits)) + for _, hit := range knnHits { + hc.knnHits[hit.ID] = hit + } + hc.computeNewScoreExpl = newScoreExplComputer +} diff --git a/search/collector/topn_test.go b/search/collector/topn_test.go new file mode 100644 index 0000000..39f6a11 --- /dev/null +++ b/search/collector/topn_test.go @@ -0,0 +1,868 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "bytes" + "context" + "testing" + + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/facet" + index "github.com/blevesearch/bleve_index_api" +) + +func TestTop10Scores(t *testing.T) { + // a stub search with more than 10 matches + // the top-10 scores are > 10 + // everything else is less than 10 + searcher := &stubSearcher{ + matches: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("a"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("b"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("c"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("d"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("e"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("f"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("g"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("h"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("i"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("j"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("k"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("l"), + Score: 99, + }, + { + IndexInternalID: index.IndexInternalID("m"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("n"), + Score: 11, + }, + }, + } + + collector := NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + err := collector.Collect(context.Background(), searcher, &stubReader{}) + if err != nil { + t.Fatal(err) + } + + maxScore := collector.MaxScore() + if maxScore != 99.0 { + t.Errorf("expected max score 99.0, got %f", maxScore) + } + + total := collector.Total() + if total != 14 { + t.Errorf("expected 14 total results, got %d", total) + } + + results := collector.Results() + + if len(results) != 10 { + t.Logf("results: %v", results) + t.Fatalf("expected 10 results, got %d", len(results)) + } + + if results[0].ID != "l" { + t.Errorf("expected first result to have ID 'l', got %s", results[0].ID) + } + + if results[0].Score != 99.0 { + t.Errorf("expected highest score to be 99.0, got %f", results[0].Score) + } + + minScore := 1000.0 + for _, result := range results { + if result.Score < minScore { + minScore = result.Score + } + } + + if minScore < 10 { + t.Errorf("expected minimum score to be higher than 10, got %f", minScore) + } +} + +func TestTop10ScoresSkip10(t *testing.T) { + // a stub search with more than 10 matches + // the top-10 scores are > 10 + // everything else is less than 10 + searcher := &stubSearcher{ + matches: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("a"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("b"), + Score: 9.5, + }, + { + IndexInternalID: index.IndexInternalID("c"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("d"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("e"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("f"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("g"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("h"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("i"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("j"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("k"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("l"), + Score: 99, + }, + { + IndexInternalID: index.IndexInternalID("m"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("n"), + Score: 11, + }, + }, + } + + collector := NewTopNCollector(10, 10, search.SortOrder{&search.SortScore{Desc: true}}) + err := collector.Collect(context.Background(), searcher, &stubReader{}) + if err != nil { + t.Fatal(err) + } + + maxScore := collector.MaxScore() + if maxScore != 99.0 { + t.Errorf("expected max score 99.0, got %f", maxScore) + } + + total := collector.Total() + if total != 14 { + t.Errorf("expected 14 total results, got %d", total) + } + + results := collector.Results() + + if len(results) != 4 { + t.Fatalf("expected 4 results, got %d", len(results)) + } + + if results[0].ID != "b" { + t.Errorf("expected first result to have ID 'b', got %s", results[0].ID) + } + + if results[0].Score != 9.5 { + t.Errorf("expected highest score to be 9.5, got %f", results[0].Score) + } +} + +func TestTop10ScoresSkip10Only9Hits(t *testing.T) { + // a stub search with only 10 matches + searcher := &stubSearcher{ + matches: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("a"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("c"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("e"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("g"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("i"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("j"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("k"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("m"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("n"), + Score: 11, + }, + }, + } + + collector := NewTopNCollector(10, 10, search.SortOrder{&search.SortScore{Desc: true}}) + err := collector.Collect(context.Background(), searcher, &stubReader{}) + if err != nil { + t.Fatal(err) + } + + total := collector.Total() + if total != 9 { + t.Errorf("expected 9 total results, got %d", total) + } + + results := collector.Results() + + if len(results) != 0 { + t.Fatalf("expected 0 results, got %d", len(results)) + } +} + +func TestPaginationSameScores(t *testing.T) { + // a stub search with more than 10 matches + // all documents have the same score + searcher := &stubSearcher{ + matches: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("a"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("b"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("c"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("d"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("e"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("f"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("g"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("h"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("i"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("j"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("k"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("l"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("m"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("n"), + Score: 5, + }, + }, + } + + // first get first 5 hits + collector := NewTopNCollector(5, 0, search.SortOrder{&search.SortScore{Desc: true}}) + err := collector.Collect(context.Background(), searcher, &stubReader{}) + if err != nil { + t.Fatal(err) + } + + total := collector.Total() + if total != 14 { + t.Errorf("expected 14 total results, got %d", total) + } + + results := collector.Results() + + if len(results) != 5 { + t.Fatalf("expected 5 results, got %d", len(results)) + } + + firstResults := make(map[string]struct{}) + for _, hit := range results { + firstResults[hit.ID] = struct{}{} + } + + // a stub search with more than 10 matches + // all documents have the same score + searcher = &stubSearcher{ + matches: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("a"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("b"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("c"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("d"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("e"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("f"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("g"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("h"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("i"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("j"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("k"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("l"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("m"), + Score: 5, + }, + { + IndexInternalID: index.IndexInternalID("n"), + Score: 5, + }, + }, + } + + // now get next 5 hits + collector = NewTopNCollector(5, 5, search.SortOrder{&search.SortScore{Desc: true}}) + err = collector.Collect(context.Background(), searcher, &stubReader{}) + if err != nil { + t.Fatal(err) + } + + total = collector.Total() + if total != 14 { + t.Errorf("expected 14 total results, got %d", total) + } + + results = collector.Results() + + if len(results) != 5 { + t.Fatalf("expected 5 results, got %d", len(results)) + } + + // make sure that none of these hits repeat ones we saw in the top 5 + for _, hit := range results { + if _, ok := firstResults[hit.ID]; ok { + t.Errorf("doc ID %s is in top 5 and next 5 result sets", hit.ID) + } + } +} + +// TestStreamResults verifies the search.DocumentMatchHandler +func TestStreamResults(t *testing.T) { + matches := []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("a"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("b"), + Score: 1, + }, + { + IndexInternalID: index.IndexInternalID("c"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("d"), + Score: 999, + }, + { + IndexInternalID: index.IndexInternalID("e"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("f"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("g"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("h"), + Score: 89, + }, + { + IndexInternalID: index.IndexInternalID("i"), + Score: 101, + }, + { + IndexInternalID: index.IndexInternalID("j"), + Score: 112, + }, + { + IndexInternalID: index.IndexInternalID("k"), + Score: 10, + }, + { + IndexInternalID: index.IndexInternalID("l"), + Score: 99, + }, + { + IndexInternalID: index.IndexInternalID("m"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("n"), + Score: 111, + }, + } + + searcher := &stubSearcher{ + matches: matches, + } + ind := 0 + docMatchHandler := func(hit *search.DocumentMatch) error { + if hit == nil { + return nil // search completed + } + if !bytes.Equal(hit.IndexInternalID, matches[ind].IndexInternalID) { + t.Errorf("%d hit IndexInternalID actual: %s, expected: %s", + ind, hit.IndexInternalID, matches[ind].IndexInternalID) + } + if hit.Score != matches[ind].Score { + t.Errorf("%d hit Score actual: %s, expected: %s", + ind, hit.IndexInternalID, matches[ind].IndexInternalID) + } + ind++ + return nil + } + + var handlerMaker search.MakeDocumentMatchHandler = func(ctx *search.SearchContext) (search.DocumentMatchHandler, bool, error) { + return docMatchHandler, false, nil + } + + ctx := context.WithValue(context.Background(), search.MakeDocumentMatchHandlerKey, handlerMaker) + + collector := NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + err := collector.Collect(ctx, searcher, &stubReader{}) + if err != nil { + t.Fatal(err) + } + + maxScore := collector.MaxScore() + if maxScore != 999.0 { + t.Errorf("expected max score 99.0, got %f", maxScore) + } + + total := collector.Total() + if int(total) != ind { + t.Errorf("expected 14 total results, got %d", total) + } + + results := collector.Results() + + if len(results) != 0 { + t.Fatalf("expected 0 results, got %d", len(results)) + } +} + +// TestCollectorChaining verifies the chaining of collectors. +// The custom DocumentMatchHandler can process every hit for +// the search query and then pass the hit to the topn collector +// to eventually have the sorted top `N` results. +func TestCollectorChaining(t *testing.T) { + matches := []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("a"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("b"), + Score: 1, + }, + { + IndexInternalID: index.IndexInternalID("c"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("d"), + Score: 999, + }, + { + IndexInternalID: index.IndexInternalID("e"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("f"), + Score: 9, + }, + { + IndexInternalID: index.IndexInternalID("g"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("h"), + Score: 89, + }, + { + IndexInternalID: index.IndexInternalID("i"), + Score: 101, + }, + { + IndexInternalID: index.IndexInternalID("j"), + Score: 112, + }, + { + IndexInternalID: index.IndexInternalID("k"), + Score: 10, + }, + { + IndexInternalID: index.IndexInternalID("l"), + Score: 99, + }, + { + IndexInternalID: index.IndexInternalID("m"), + Score: 11, + }, + { + IndexInternalID: index.IndexInternalID("n"), + Score: 111, + }, + } + + searcher := &stubSearcher{ + matches: matches, + } + + var topNHandler search.DocumentMatchHandler + ind := 0 + docMatchHandler := func(hit *search.DocumentMatch) error { + if hit == nil { + return nil // search completed + } + if !bytes.Equal(hit.IndexInternalID, matches[ind].IndexInternalID) { + t.Errorf("%d hit IndexInternalID actual: %s, expected: %s", + ind, hit.IndexInternalID, matches[ind].IndexInternalID) + } + if hit.Score != matches[ind].Score { + t.Errorf("%d hit Score actual: %s, expected: %s", + ind, hit.IndexInternalID, matches[ind].IndexInternalID) + } + ind++ + // give the hit back to the topN collector + err := topNHandler(hit) + if err != nil { + t.Errorf("unexpected err: %v", err) + } + return nil + } + + var handlerMaker search.MakeDocumentMatchHandler = func(ctx *search.SearchContext) (search.DocumentMatchHandler, bool, error) { + topNHandler, _, _ = MakeTopNDocumentMatchHandler(ctx) + return docMatchHandler, false, nil + } + + ctx := context.WithValue(context.Background(), search.MakeDocumentMatchHandlerKey, + handlerMaker) + + collector := NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + err := collector.Collect(ctx, searcher, &stubReader{}) + if err != nil { + t.Fatal(err) + } + + maxScore := collector.MaxScore() + if maxScore != 999.0 { + t.Errorf("expected max score 99.0, got %f", maxScore) + } + + total := collector.Total() + if int(total) != ind { + t.Errorf("expected 14 total results, got %d", total) + } + + results := collector.Results() + + if len(results) != 10 { // as it is paged + t.Fatalf("expected 0 results, got %d", len(results)) + } + + if results[0].ID != "d" { + t.Errorf("expected first result to have ID 'l', got %s", results[0].ID) + } + + if results[0].Score != 999.0 { + t.Errorf("expected highest score to be 999.0, got %f", results[0].Score) + } + + minScore := 1000.0 + for _, result := range results { + if result.Score < minScore { + minScore = result.Score + } + } + + if minScore < 10 { + t.Errorf("expected minimum score to be higher than 10, got %f", minScore) + } +} + +func setupIndex(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + scorch.Name, + map[string]interface{}{ + "path": "", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + return i +} + +func TestSetFacetsBuilder(t *testing.T) { + // Field common to both sorting and faceting. + sortFacetsField := "locations" + + coll := NewTopNCollector(10, 0, search.SortOrder{&search.SortField{Field: sortFacetsField}}) + + i := setupIndex(t) + indexReader, err := i.Reader() + if err != nil { + t.Fatal(err) + } + + fb := search.NewFacetsBuilder(indexReader) + facetBuilder := facet.NewTermsFacetBuilder(sortFacetsField, 100) + fb.Add("locations_facet", facetBuilder) + coll.SetFacetsBuilder(fb) + + // Should not duplicate the "locations" field in the collector. + if len(coll.neededFields) != 1 || coll.neededFields[0] != sortFacetsField { + t.Errorf("expected fields in collector: %v, observed: %v", []string{sortFacetsField}, coll.neededFields) + } +} + +func BenchmarkTop10of0Scores(b *testing.B) { + benchHelper(0, func() search.Collector { + return NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10of3Scores(b *testing.B) { + benchHelper(3, func() search.Collector { + return NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10of10Scores(b *testing.B) { + benchHelper(10, func() search.Collector { + return NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10of25Scores(b *testing.B) { + benchHelper(25, func() search.Collector { + return NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10of50Scores(b *testing.B) { + benchHelper(50, func() search.Collector { + return NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10of10000Scores(b *testing.B) { + benchHelper(10000, func() search.Collector { + return NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop100of0Scores(b *testing.B) { + benchHelper(0, func() search.Collector { + return NewTopNCollector(100, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop100of3Scores(b *testing.B) { + benchHelper(3, func() search.Collector { + return NewTopNCollector(100, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop100of10Scores(b *testing.B) { + benchHelper(10, func() search.Collector { + return NewTopNCollector(100, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop100of25Scores(b *testing.B) { + benchHelper(25, func() search.Collector { + return NewTopNCollector(100, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop100of50Scores(b *testing.B) { + benchHelper(50, func() search.Collector { + return NewTopNCollector(100, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop100of10000Scores(b *testing.B) { + benchHelper(10000, func() search.Collector { + return NewTopNCollector(100, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop1000of10000Scores(b *testing.B) { + benchHelper(10000, func() search.Collector { + return NewTopNCollector(1000, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10000of100000Scores(b *testing.B) { + benchHelper(100000, func() search.Collector { + return NewTopNCollector(10000, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10of100000Scores(b *testing.B) { + benchHelper(100000, func() search.Collector { + return NewTopNCollector(10, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop100of100000Scores(b *testing.B) { + benchHelper(100000, func() search.Collector { + return NewTopNCollector(100, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop1000of100000Scores(b *testing.B) { + benchHelper(100000, func() search.Collector { + return NewTopNCollector(1000, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} + +func BenchmarkTop10000of1000000Scores(b *testing.B) { + benchHelper(1000000, func() search.Collector { + return NewTopNCollector(10000, 0, search.SortOrder{&search.SortScore{Desc: true}}) + }, b) +} diff --git a/search/explanation.go b/search/explanation.go new file mode 100644 index 0000000..9240500 --- /dev/null +++ b/search/explanation.go @@ -0,0 +1,56 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/size" +) + +var reflectStaticSizeExplanation int + +func init() { + var e Explanation + reflectStaticSizeExplanation = int(reflect.TypeOf(e).Size()) +} + +type Explanation struct { + Value float64 `json:"value"` + Message string `json:"message"` + PartialMatch bool `json:"partial_match,omitempty"` + Children []*Explanation `json:"children,omitempty"` +} + +func (expl *Explanation) String() string { + js, err := json.MarshalIndent(expl, "", " ") + if err != nil { + return fmt.Sprintf("error serializing explanation to json: %v", err) + } + return string(js) +} + +func (expl *Explanation) Size() int { + sizeInBytes := reflectStaticSizeExplanation + size.SizeOfPtr + + len(expl.Message) + + for _, entry := range expl.Children { + sizeInBytes += entry.Size() + } + + return sizeInBytes +} diff --git a/search/facet/benchmark_data.txt b/search/facet/benchmark_data.txt new file mode 100644 index 0000000..b012f78 --- /dev/null +++ b/search/facet/benchmark_data.txt @@ -0,0 +1,2909 @@ +Boiling liquid expanding vapor explosion +From Wikipedia, the free encyclopedia +See also: Boiler explosion and Steam explosion + +Flames subsequent to a flammable liquid BLEVE from a tanker. BLEVEs do not necessarily involve fire. + +This article's tone or style may not reflect the encyclopedic tone used on Wikipedia. See Wikipedia's guide to writing better articles for suggestions. (July 2013) +A boiling liquid expanding vapor explosion (BLEVE, /ˈblɛviː/ blev-ee) is an explosion caused by the rupture of a vessel containing a pressurized liquid above its boiling point.[1] +Contents [hide] +1 Mechanism +1.1 Water example +1.2 BLEVEs without chemical reactions +2 Fires +3 Incidents +4 Safety measures +5 See also +6 References +7 External links +Mechanism[edit] + +This section needs additional citations for verification. Please help improve this article by adding citations to reliable sources. Unsourced material may be challenged and removed. (July 2013) +There are three characteristics of liquids which are relevant to the discussion of a BLEVE: +If a liquid in a sealed container is boiled, the pressure inside the container increases. As the liquid changes to a gas it expands - this expansion in a vented container would cause the gas and liquid to take up more space. In a sealed container the gas and liquid are not able to take up more space and so the pressure rises. Pressurized vessels containing liquids can reach an equilibrium where the liquid stops boiling and the pressure stops rising. This occurs when no more heat is being added to the system (either because it has reached ambient temperature or has had a heat source removed). +The boiling temperature of a liquid is dependent on pressure - high pressures will yield high boiling temperatures, and low pressures will yield low boiling temperatures. A common simple experiment is to place a cup of water in a vacuum chamber, and then reduce the pressure in the chamber until the water boils. By reducing the pressure the water will boil even at room temperature. This works both ways - if the pressure is increased beyond normal atmospheric pressures, the boiling of hot water could be suppressed far beyond normal temperatures. The cooling system of a modern internal combustion engine is a real-world example. +When a liquid boils it turns into a gas. The resulting gas takes up far more space than the liquid did. +Typically, a BLEVE starts with a container of liquid which is held above its normal, atmospheric-pressure boiling temperature. Many substances normally stored as liquids, such as CO2, oxygen, and other similar industrial gases have boiling temperatures, at atmospheric pressure, far below room temperature. In the case of water, a BLEVE could occur if a pressurized chamber of water is heated far beyond the standard 100 °C (212 °F). That container, because the boiling water pressurizes it, is capable of holding liquid water at very high temperatures. +If the pressurized vessel, containing liquid at high temperature (which may be room temperature, depending on the substance) ruptures, the pressure which prevents the liquid from boiling is lost. If the rupture is catastrophic, where the vessel is immediately incapable of holding any pressure at all, then there suddenly exists a large mass of liquid which is at very high temperature and very low pressure. This causes the entire volume of liquid to instantaneously boil, which in turn causes an extremely rapid expansion. Depending on temperatures, pressures and the substance involved, that expansion may be so rapid that it can be classified as an explosion, fully capable of inflicting severe damage on its surroundings. +Water example[edit] +Imagine, for example, a tank of pressurized liquid water held at 204.4 °C (400 °F). This vessel would normally be pressurized to 1.7 MPa (250 psi) above atmospheric ("gauge") pressure. Were the tank containing the water to split open, there would momentarily exist a volume of liquid water which is +at atmospheric pressure, and +204.4 °C (400 °F). +At atmospheric pressure the boiling point of water is 100 °C (212 °F) - liquid water at atmospheric pressure cannot exist at temperatures higher than 100 °C (212 °F). It is obvious, then, that 204.4 °C (400 °F) liquid water at atmospheric pressure must immediately flash to gas causing an explosion. +BLEVEs without chemical reactions[edit] +It is important to note that a BLEVE need not be a chemical explosion - nor does there need to be a fire - however if a flammable substance is subject to a BLEVE it may also be subject to intense heating, either from an external source of heat which may have caused the vessel to rupture in the first place or from an internal source of localized heating such as skin friction. This heating can cause a flammable substance to ignite, adding a secondary explosion caused by the primary BLEVE. While blast effects of any BLEVE can be devastating, a flammable substance such as propane can add significantly to the danger. +Bleve explosion.svg +While the term BLEVE is most often used to describe the results of a container of flammable liquid rupturing due to fire, a BLEVE can occur even with a non-flammable substance such as water,[2] liquid nitrogen,[3] liquid helium or other refrigerants or cryogens, and therefore is not usually considered a type of chemical explosion. +Fires[edit] +BLEVEs can be caused by an external fire near the storage vessel causing heating of the contents and pressure build-up. While tanks are often designed to withstand great pressure, constant heating can cause the metal to weaken and eventually fail. If the tank is being heated in an area where there is no liquid, it may rupture faster without the liquid to absorb the heat. Gas containers are usually equipped with relief valves that vent off excess pressure, but the tank can still fail if the pressure is not released quickly enough.[1] Relief valves are sized to release pressure fast enough to prevent the pressure from increasing beyond the strength of the vessel, but not so fast as to be the cause of an explosion. An appropriately sized relief valve will allow the liquid inside to boil slowly, maintaining a constant pressure in the vessel until all the liquid has boiled and the vessel empties. +If the substance involved is flammable, it is likely that the resulting cloud of the substance will ignite after the BLEVE has occurred, forming a fireball and possibly a fuel-air explosion, also termed a vapor cloud explosion (VCE). If the materials are toxic, a large area will be contaminated.[4] +Incidents[edit] +The term "BLEVE" was coined by three researchers at Factory Mutual, in the analysis of an accident there in 1957 involving a chemical reactor vessel.[5] +In August 1959 the Kansas City Fire Department suffered its largest ever loss of life in the line of duty, when a 25,000 gallon (95,000 litre) gas tank exploded during a fire on Southwest Boulevard killing five firefighters. This was the first time BLEVE was used to describe a burning fuel tank.[citation needed] +Later incidents included the Cheapside Street Whisky Bond Fire in Glasgow, Scotland in 1960; Feyzin, France in 1966; Crescent City, Illinois in 1970; Kingman, Arizona in 1973; a liquid nitrogen tank rupture[6] at Air Products and Chemicals and Mobay Chemical Company at New Martinsville, West Virginia on January 31, 1978 [1];Texas City, Texas in 1978; Murdock, Illinois in 1983; San Juan Ixhuatepec, Mexico City in 1984; and Toronto, Ontario in 2008. +Safety measures[edit] +[icon] This section requires expansion. (July 2013) +Some fire mitigation measures are listed under liquefied petroleum gas. +See also[edit] +Boiler explosion +Expansion ratio +Explosive boiling or phase explosion +Rapid phase transition +Viareggio train derailment +2008 Toronto explosions +Gas carriers +Los Alfaques Disaster +Lac-Mégantic derailment +References[edit] +^ Jump up to: a b Kletz, Trevor (March 1990). Critical Aspects of Safety and Loss Prevention. London: Butterworth–Heinemann. pp. 43–45. ISBN 0-408-04429-2. +Jump up ^ "Temperature Pressure Relief Valves on Water Heaters: test, inspect, replace, repair guide". Inspect-ny.com. Retrieved 2011-07-12. +Jump up ^ Liquid nitrogen BLEVE demo +Jump up ^ "Chemical Process Safety" (PDF). Retrieved 2011-07-12. +Jump up ^ David F. Peterson, BLEVE: Facts, Risk Factors, and Fallacies, Fire Engineering magazine (2002). +Jump up ^ "STATE EX REL. VAPOR CORP. v. NARICK". Supreme Court of Appeals of West Virginia. 1984-07-12. Retrieved 2014-03-16. +External links[edit] + Look up boiling liquid expanding vapor explosion in Wiktionary, the free dictionary. + Wikimedia Commons has media related to BLEVE. +BLEVE Demo on YouTube — video of a controlled BLEVE demo +huge explosions on YouTube — video of propane and isobutane BLEVEs from a train derailment at Murdock, Illinois (3 September 1983) +Propane BLEVE on YouTube — video of BLEVE from the Toronto propane depot fire +Moscow Ring Road Accident on YouTube - Dozens of LPG tank BLEVEs after a road accident in Moscow +Kingman, AZ BLEVE — An account of the 5 July 1973 explosion in Kingman, with photographs +Propane Tank Explosions — Description of circumstances required to cause a propane tank BLEVE. +Analysis of BLEVE Events at DOE Sites - Details physics and mathematics of BLEVEs. +HID - SAFETY REPORT ASSESSMENT GUIDE: Whisky Maturation Warehouses - The liquor is aged in wooden barrels that can suffer BLEVE. +Categories: ExplosivesFirefightingFireTypes of fireGas technologiesIndustrial fires and explosions +Navigation menu +Create accountLog inArticleTalkReadEditView history + +Main page +Contents +Featured content +Current events +Random article +Donate to Wikipedia +Wikimedia Shop +Interaction +Help +About Wikipedia +Community portal +Recent changes +Contact page +Tools +What links here +Related changes +Upload file +Special pages +Permanent link +Page information +Wikidata item +Cite this page +Print/export +Create a book +Download as PDF +Printable version +Languages +Català +Deutsch +Español +Français +Italiano +עברית +Nederlands +日本語 +Norsk bokmål +Polski +Português +Русский +Suomi +Edit links +This page was last modified on 18 November 2014 at 01:35. +Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc., a non-profit organization. +Privacy policyAbout WikipediaDisclaimersContact WikipediaDevelopersMobile viewWikimedia Foundation Powered by MediaWiki + + +Thermobaric weapon +From Wikipedia, the free encyclopedia + +Blast from a US Navy fuel air explosive used against a decommissioned ship, USS McNulty, 1972. +A thermobaric weapon is a type of explosive that utilizes oxygen from the surrounding air to generate an intense, high-temperature explosion, and in practice the blast wave such a weapon produces is typically significantly longer in duration than a conventional condensed explosive. The fuel-air bomb is one of the most well-known types of thermobaric weapons. +Most conventional explosives consist of a fuel-oxidizer premix (gunpowder, for example, contains 25% fuel and 75% oxidizer), whereas thermobaric weapons are almost 100% fuel, so thermobaric weapons are significantly more energetic than conventional condensed explosives of equal weight. Their reliance on atmospheric oxygen makes them unsuitable for use underwater, at high altitude, and in adverse weather. They do, however, cause considerably more destruction when used inside confined environments such as tunnels, caves, and bunkers - partly due to the sustained blast wave, and partly by consuming the available oxygen inside those confined spaces. +There are many different types of thermobaric weapons rounds that can be fitted to hand-held launchers.[1] +Contents [hide] +1 Terminology +2 Mechanism +2.1 Fuel-air explosive +2.1.1 Effect +3 Development history +3.1 Soviet and Russian developments +3.2 US developments +4 History +4.1 Military use +4.2 Non-military use +5 See also +6 References +7 External links +Terminology[edit] +The term thermobaric is derived from the Greek words for "heat" and "pressure": thermobarikos (θερμοβαρικός), from thermos (θερμός), hot + baros (βάρος), weight, pressure + suffix -ikos (-ικός), suffix -ic. +Other terms used for this family of weapons are high-impulse thermobaric weapons (HITs), heat and pressure weapons, vacuum bombs, or fuel-air explosives (FAE or FAX). +Mechanism[edit] +In contrast to condensed explosive, where oxidation in a confined region produces a blast front from essentially a point source, a flame front accelerates to a large volume producing pressure fronts both within the mixture of fuel and oxidant and then in the surrounding air.[2] +Thermobaric explosives apply the principles underlying accidental unconfined vapor cloud explosions, which include those from dispersions of flammable dusts and droplets.[3] Previously, such explosions were most often encountered in flour mills and their storage containers, and later in coal mines; but, now, most commonly in discharged oil tankers and refineries, including an incident at Buncefield in the UK in 2005 where the blast wave woke people 150 kilometres (93 mi) from its centre.[4] +A typical weapon consists of a container packed with a fuel substance, in the center of which is a small conventional-explosive "scatter charge". Fuels are chosen on the basis of the exothermicity of their oxidation, ranging from powdered metals, such as aluminium or magnesium, to organic materials, possibly with a self-contained partial oxidant. The most recent development involves the use of nanofuels.[5][6] +A thermobaric bomb's effective yield requires the most appropriate combination of a number of factors; among these are how well the fuel is dispersed, how rapidly it mixes with the surrounding atmosphere, and the initiation of the igniter and its position relative to the container of fuel. In some designs, strong munitions cases allow the blast pressure to be contained long enough for the fuel to be heated up well above its auto-ignition temperature, so that once the container bursts the super-heated fuel will auto-ignite progressively as it comes into contact with atmospheric oxygen.[7][8][9][10][11][12][13][14][15][16][17] +Conventional upper and lower limits of flammability apply to such weapons. Close in, blast from the dispersal charge, compressing and heating the surrounding atmosphere, will have some influence on the lower limit. The upper limit has been demonstrated strongly to influence the ignition of fogs above pools of oil.[18] This weakness may be eliminated by designs where the fuel is preheated well above its ignition temperature, so that its cooling during its dispersion still results in a minimal ignition delay on mixing. The continual combustion of the outer layer of fuel molecules as they come into contact with the air, generates additional heat which maintains the temperature of the interior of the fireball, and thus sustains the detonation.[19][20][21] +In confinement, a series of reflective shock waves are generated,[22][23] which maintain the fireball and can extend its duration to between 10 and 50 ms as exothermic recombination reactions occur.[24] Further damage can result as the gases cool and pressure drops sharply, leading to a partial vacuum. This effect has given rise to the misnomer "vacuum bomb". Piston-type afterburning is also believed to occur in such structures, as flame-fronts accelerate through it.[25][26] +Fuel-air explosive[edit] +A fuel-air explosive (FAE) device consists of a container of fuel and two separate explosive charges. After the munition is dropped or fired, the first explosive charge bursts open the container at a predetermined height and disperses the fuel in a cloud that mixes with atmospheric oxygen (the size of the cloud varies with the size of the munition). The cloud of fuel flows around objects and into structures. The second charge then detonates the cloud, creating a massive blast wave. The blast wave destroys unreinforced buildings and equipment and kills and injures people. The antipersonnel effect of the blast wave is more severe in foxholes, on people with body armor, and in enclosed spaces such as caves, buildings, and bunkers. +Fuel-air explosives were first developed, and used in Vietnam, by the United States. Soviet scientists, however, quickly developed their own FAE weapons, which were reportedly used against China in the Sino-Soviet border conflict and in Afghanistan. Since then, research and development has continued and currently Russian forces field a wide array of third-generation FAE warheads. +Effect[edit] +A Human Rights Watch report of 1 February 2000[27] quotes a study made by the US Defense Intelligence Agency: +The [blast] kill mechanism against living targets is unique–and unpleasant.... What kills is the pressure wave, and more importantly, the subsequent rarefaction [vacuum], which ruptures the lungs.... If the fuel deflagrates but does not detonate, victims will be severely burned and will probably also inhale the burning fuel. Since the most common FAE fuels, ethylene oxide and propylene oxide, are highly toxic, undetonated FAE should prove as lethal to personnel caught within the cloud as most chemical agents. +According to a U.S. Central Intelligence Agency study,[27] "the effect of an FAE explosion within confined spaces is immense. Those near the ignition point are obliterated. Those at the fringe are likely to suffer many internal, and thus invisible injuries, including burst eardrums and crushed inner ear organs, severe concussions, ruptured lungs and internal organs, and possibly blindness." Another Defense Intelligence Agency document speculates that because the "shock and pressure waves cause minimal damage to brain tissue…it is possible that victims of FAEs are not rendered unconscious by the blast, but instead suffer for several seconds or minutes while they suffocate."[28] +Development history[edit] +Soviet and Russian developments[edit] + +A RPO-A rocket and launcher. +The Soviet armed forces extensively developed FAE weapons,[29] such as the RPO-A, and used them in Chechnya.[30] +The Russian armed forces have developed thermobaric ammunition variants for several of their weapons, such as the TGB-7V thermobaric grenade with a lethality radius of 10 metres (33 ft), which can be launched from a RPG-7. The GM-94 is a 43 mm pump-action grenade launcher which is designed mainly to fire thermobaric grenades for close quarters combat. With the grenade weighing 250 grams (8.8 oz) and holding a 160 grams (5.6 oz) explosive mixture, its lethality radius is 3 metres (9.8 ft); however, due to the deliberate "fragmentation-free" design of the grenade, 4 metres (13 ft) is already considered a safe distance.[31] The RPO-A and upgraded RPO-M are infantry-portable RPGs designed to fire thermobaric rockets. The RPO-M, for instance, has a thermobaric warhead with a TNT equivalence of 5.5 kilograms (12 lb) of TNT and destructive capabilities similar to a 152 mm High explosive fragmentation artillery shell.[32][33] The RShG-1 and the RShG-2 are thermobaric variants of the RPG-27 and RPG-26 respectively. The RShG-1 is the more powerful variant, with its warhead having a 10 metres (33 ft) lethality radius and producing about the same effect as 6 kg (13 lb) of TNT.[34] The RMG is a further derivative of the RPG-26 that uses a tandem-charge warhead, whereby the precursor HEAT warhead blasts an opening for the main thermobaric charge to enter and detonate inside.[35] The RMG's precursor HEAT warhead can penetrate 300 mm of reinforced concrete or over 100 mm of Rolled homogeneous armour, thus allowing the 105 millimetres (4.1 in) diameter thermobaric warhead to detonate inside.[36] +The other examples include the SACLOS or millimeter wave radar-guided thermobaric variants of the 9M123 Khrizantema, the 9M133F-1 thermobaric warhead variant of the 9M133 Kornet, and the 9M131F thermobaric warhead variant of the 9K115-2 Metis-M, all of which are anti-tank missiles. The Kornet has since been upgraded to the Kornet-EM, and its thermobaric variant has a maximum range of 10 kilometres (6.2 mi) and has the TNT equivalent of 7 kilograms (15 lb) of TNT.[37] The 300 mm 9M55S thermobaric cluster warhead rocket was built to be fired from the BM-30 Smerch MLRS. A dedicated carrier of thermobaric weapons is the purpose-built TOS-1, a 24-tube MLRS designed to fire 220 mm caliber thermobaric rockets. A full salvo from the TOS-1 will cover a rectangle 200x400 metres.[38] The Iskander-M theatre ballistic missile can also carry a 700 kilograms (1,500 lb) thermobaric warhead.[39] + +The fireball blast from the Russian Air Force's FOAB, the largest Thermobaric device to be detonated. +Many Russian Air Force munitions also have thermobaric variants. The 80 mm S-8 rocket has the S-8DM and S-8DF thermobaric variants. The S-8's larger 122 mm brother, the S-13 rocket, has the S-13D and S-13DF thermobaric variants. The S-13DF's warhead weighs only 32 kg (71 lb) but its power is equivalent to 40 kg (88 lb) of TNT. The KAB-500-OD variant of the KAB-500KR has a 250 kg (550 lb) thermobaric warhead. The ODAB-500PM and ODAB-500PMV unguided bombs carry a 190 kg (420 lb) fuel-air explosive each. The KAB-1500S GLONASS/GPS guided 1,500 kg (3,300 lb) bomb also has a thermobaric variant. Its fireball will cover over a 150-metre (490 ft) radius and its lethality zone is a 500-metre (1,600 ft) radius.[40] The 9M120 Ataka-V and the 9K114 Shturm ATGMs both have thermobaric variants. +In September 2007 Russia exploded the largest thermobaric weapon ever made. The weapon's yield was reportedly greater than that of the smallest dial-a-yield nuclear weapons at their lowest settings.[41][42] Russia named this particular ordnance the "Father of All Bombs" in response to the United States developed "Massive Ordnance Air Blast" (MOAB) bomb whose backronym is the "Mother of All Bombs", and which previously held the accolade of the most powerful non-nuclear weapon in history.[43] The bomb contains an about 7 tons charge of a liquid fuel such as ethylene oxide, mixed with an energetic nanoparticle such as aluminium, surrounding a high explosive burster[44] that when detonated created an explosion equivalent to 44 metric tons of TNT. +US developments[edit] + +A BLU-72/B bomb on a USAF A-1E taking off from Nakhon Phanom, in September 1968. +Current US FAE munitions include: +BLU-73 FAE I +BLU-95 500-lb (FAE-II) +BLU-96 2,000-lb (FAE-II) +CBU-55 FAE I +CBU-72 FAE I +The XM1060 40-mm grenade is a small-arms thermobaric device, which was delivered to U.S. forces in April 2003.[45] Since the 2003 Invasion of Iraq, the US Marine Corps has introduced a thermobaric 'Novel Explosive' (SMAW-NE) round for the Mk 153 SMAW rocket launcher. One team of Marines reported that they had destroyed a large one-story masonry type building with one round from 100 yards (91 m).[46] +The AGM-114N Hellfire II, first used by U.S. forces in 2003 in Iraq, uses a Metal Augmented Charge (MAC) warhead that contains a thermobaric explosive fill using fluoridated aluminium layered between the charge casing and a PBXN-112 explosive mixture. When the PBXN-112 detonates, the aluminium mixture is dispersed and rapidly burns. The resultant sustained high pressure is extremely effective against people and structures.[47] +History[edit] +Military use[edit] + +US Navy BLU-118B being prepared for shipping for use in Afghanistan, 5 March 2002. +The first experiments with thermobaric weapon were conducted in Germany during World War II and were led by Mario Zippermayr. The German bombs used coal dust as fuel and were extensively tested in 1943 and 1944, but did not reach mass production before the war ended. +The TOS-1 system was test fired in Panjshir valley during Soviet war in Afghanistan in the early 1980s.[48] +Unconfirmed reports suggest that Russian military forces used ground delivered thermobaric weapons in the storming of the Russian parliament during the 1993 Russian constitutional crisis and also during the Battle for Grozny (first and second Chechen wars) to attack dug in Chechen fighters. The use of both TOS-1 heavy MLRS and "RPO-A Shmel" shoulder-fired rocket system in the Chechen wars is reported to have occurred.[48][49] +It is theorized that a multitude of hand-held thermobaric weapons were used by the Russian Armed Forces in their efforts to retake the school during the 2004 Beslan school hostage crisis. The RPO-A and either the TGB-7V thermobaric rocket from the RPG-7 or rockets from either the RShG-1 or the RShG-2 is claimed to have been used by the Spetsnaz during the initial storming of the school.[50][51][52] At least 3 and as many as 9 RPO-A casings were later found at the positions of the Spetsnaz.[53][54] The Russian Government later admitted to the use of the RPO-A during the crisis.[55] +According to UK Ministry of Defence, British military forces have also used thermobaric weapons in their AGM-114N Hellfire missiles (carried by Apache helicopters and UAVs) against the Taliban in the War in Afghanistan.[56] +The US military also used thermobaric weapons in Afghanistan. On 3 March 2002, a single 2,000 lb (910 kg) laser guided thermobaric bomb was used by the United States Army against cave complexes in which Al-Qaeda and Taliban fighters had taken refuge in the Gardez region of Afghanistan.[57][58] The SMAW-NE was used by the US Marines during the First Battle of Fallujah and Second Battle of Fallujah. +Reports by the rebel fighters of the Free Syrian Army claim the Syrian Air Force used such weapons against residential area targets occupied by the rebel fighters, as for instance in the Battle for Aleppo[59] and also in Kafar Batna.[60] A United Nations panel of human rights investigators reported that the Syrian government used thermobaric bombs against the rebellious town of Qusayr in March 2013.[61] +Non-military use[edit] +Thermobaric and fuel-air explosives have been used in guerrilla warfare since the 1983 Beirut barracks bombing in Lebanon, which used a gas-enhanced explosive mechanism, probably propane, butane or acetylene.[62] The explosive used by the bombers in the 1993 World Trade Center bombing incorporated the FAE principle, using three tanks of bottled hydrogen gas to enhance the blast.[63][64] Jemaah Islamiyah bombers used a shock-dispersed solid fuel charge,[65] based on the thermobaric principle,[66] to attack the Sari nightclub in the 2002 Bali bombings.[67] +See also[edit] +Bunker buster +Dust explosion +FOAB +Flame fougasse +MOAB +RPO-A +SMAW +References[edit] +Jump up ^ Algeria Isp (2011-10-18). "Libye – l'Otan utilise une bombe FAE | Politique, Algérie". Algeria ISP. Retrieved 2013-04-23. +Jump up ^ Nettleton, J. Occ. Accidents, 1, 149 (1976). +Jump up ^ Strehlow, 14th. Symp. (Int.) Comb. 1189, Comb. Inst. (1973). +Jump up ^ Health and Safety Environmental Agency, 5th. and final report, 2008. +Jump up ^ See Nanofuel/Oxidizers For Energetic Compositions – John D. Sullivan and Charles N. Kingery (1994) High explosive disseminator for a high explosive air bomb. +Jump up ^ Slavica Terzić, Mirjana Dakić Kolundžija, Milovan Azdejković and Gorgi Minov (2004) Compatibility Of Thermobaric Mixtures Based On Isopropyl Nitrate And Metal Powders. +Jump up ^ Meyer, Rudolf; Josef Köhler and Axel Homburg (2007). Explosives. Weinheim: Wiley-VCH. pp. 312. ISBN 3-527-31656-6. OCLC 165404124. +Jump up ^ Howard C. Hornig (1998) Non-focusing active warhead. +Jump up ^ Chris Ludwig (Talley Defense) Verifying Performance of Thermobaric Materials for Small to Medium Caliber Rocket Warheads. +Jump up ^ Martin M.West (1982) Composite high explosives for high energy blast applications. +Jump up ^ Raafat H. Guirguis (2005) Reactively Induced Fragmenting Explosives. +Jump up ^ Michael Dunning, William Andrews and Kevin Jaansalu (2005) The Fragmentation of Metal Cylinders Using Thermobaric Explosives. +Jump up ^ David L. Frost, Fan Zhang, Stephen B. Murray and Susan McCahan Critical Conditions For Ignition Of Metal Particles In A Condensed Explosive. +Jump up ^ The Army Doctrine and Training Bulletin (2001) The Threat from Blast Weapons. +Jump up ^ INTERNATIONAL DEFENCE REVIEW (2004) ENHANCED BLAST AND THERMOBARICS. +Jump up ^ F. Winterberg Conjectured Metastable Super-Explosives formed under High Pressure for Thermonuclear Ignition. +Jump up ^ Zhang, Fan (Medicine Hat, CA) Murray, Stephen Burke (Medicine Hat, CA) Higgins, Andrew (Montreal, CA) (2005) Super compressed detonation method and device to effect such detonation. +Jump up ^ Nettleton, arch. combust.,1,131, (1981). +Jump up ^ Stephen B. Murray Fundamental and Applied Studies of Fuel-Air Detonation. +Jump up ^ John H. Lee (1992) Chemical initiation of detonation in fuel-air explosive clouds. +Jump up ^ Frank E. Lowther (1989) Nuclear-sized explosions without radiation. +Jump up ^ Nettleton, Comb. and Flame, 24,65 (1975). +Jump up ^ Fire Prev. Sci. and Tech. No. 19,4 (1976) +Jump up ^ May L.Chan (2001) Advanced Thermobaric Explosive Compositions. +Jump up ^ New Thermobaric Materials and Weapon Concepts. +Jump up ^ Robert C. Morris (2003) Small Thermobaric Weapons An Unnoticed Threat.[dead link] +^ Jump up to: a b "Backgrounder on Russian Fuel Air Explosives ("Vacuum Bombs") | Human Rights Watch". Hrw.org. 2000-02-01. Retrieved 2013-04-23. +Jump up ^ Defense Intelligence Agency, "Future Threat to the Soldier System, Volume I; Dismounted Soldier--Middle East Threat", September 1993, p. 73. Obtained by Human Rights Watch under the U.S. Freedom of Information Act. +Jump up ^ "Press | Human Rights Watch". Hrw.org. 2008-12-27. Retrieved 2009-07-30. +Jump up ^ Lester W. Grau and Timothy L. Thomas(2000)"Russian Lessons Learned From the Battles For Grozny" +Jump up ^ "Modern Firearms – GM-94". World.guns.ru. 2011-01-24. Retrieved 2011-07-12. +Jump up ^ "New RPO Shmel-M Infantry Rocket Flamethrower Man-Packable Thermobaric Weapon". defensereview.com. 2006-07-19. Retrieved 2012-08-27. +Jump up ^ "Shmel-M: Infantry Rocket-assisted Flamethrower of Enhanced Range and Lethality". Kbptula.ru. Retrieved 2013-12-28. +Jump up ^ "Modern Firearms – RShG-1". World.guns.ru. 2011-01-24. Retrieved 2011-07-12. +Jump up ^ "Modern Firearms – RMG". World.guns.ru. 2011-01-24. Retrieved 2011-07-12. +Jump up ^ "RMG - A new Multi-Purpose Assault Weapon from Bazalt". defense-update.com. Retrieved 2012-08-27. +Jump up ^ "Kornet-EM: Multi-purpose Long-range Missile System". Kbptula.ru. Retrieved 2013-12-28. +Jump up ^ "TOS-1 Heavy flamethrower system". military-today.com. Retrieved 2012-08-27. +Jump up ^ "SS-26". Missilethreat.com. Retrieved 2013-12-28. +Jump up ^ Air Power Australia (2007-07-04). "How to Destroy the Australian Defence Force". Ausairpower.net. Retrieved 2011-07-12. +Jump up ^ "Russia unveils devastating vacuum bomb". ABC News. 2007. Retrieved 2007-09-12. +Jump up ^ "Video of test explosion". BBC News. 2007. Retrieved 2007-09-12. +Jump up ^ Harding, Luke (2007-09-12). "Russia unveils the father of all bombs". London: The Guardian. Retrieved 2007-09-12. +Jump up ^ Berhie, Saba. "Dropping the Big One | Popular Science". Popsci.com. Retrieved 2011-07-12. +Jump up ^ John Pike (2003-04-22). "XM1060 40mm Thermobaric Grenade". Globalsecurity.org. Retrieved 2011-07-12. +Jump up ^ David Hambling (2005) "Marines Quiet About Brutal New Weapon" +Jump up ^ John Pike (2001-09-11). "AGM-114N Metal Augmented Charge (MAC) Thermobaric Hellfire". Globalsecurity.org. Retrieved 2011-07-12. +^ Jump up to: a b John Pike. "TOS-1 Buratino 220mm Multiple Rocket Launcher". Globalsecurity.org. Retrieved 2013-04-23. +Jump up ^ "Foreign Military Studies Office Publications - A 'Crushing' Victory: Fuel-Air Explosives and Grozny 2000". Fmso.leavenworth.army.mil. Retrieved 2013-04-23. +Jump up ^ "Russian forces faulted in Beslan school tragedy". Christian Science Monitor. 1 September 2006. Retrieved 14 February 2007. +Jump up ^ Russia: Independent Beslan Investigation Sparks Controversy, The Jamestown Foundation, 29 August 2006 +Jump up ^ Beslan still a raw nerve for Russia, BBC News, 1 September 2006 +Jump up ^ ACHING TO KNOW, Los Angeles Times, 27 August 2005 +Jump up ^ Searching for Traces of “Shmel” in Beslan School, Kommersant, 12 September 2005 +Jump up ^ A Reversal Over Beslan Only Fuels Speculation, The Moscow Times, 21 July 2005 +Jump up ^ "MoD's Controversial Thermobaric Weapons Use in Afghanistan". Armedforces-int.com. 2008-06-23. Retrieved 2013-04-23. +Jump up ^ "US Uses Bunker-Busting 'Thermobaric' Bomb for First Time". Commondreams.org. 2002-03-03. Retrieved 2013-04-23. +Jump up ^ John Pike. "BLU-118/B Thermobaric Weapon Demonstration / Hard Target Defeat Program". Globalsecurity.org. Retrieved 2013-04-23. +Jump up ^ "Syria rebels say Assad using 'mass-killing weapons' in Aleppo". October 10, 2012. Retrieved November 11, 2012. +Jump up ^ "Dropping Thermobaric Bombs on Residential Areas in Syria_ Nov. 5. 2012". First Post. November 11, 2012. Retrieved November 11, 2012. +Jump up ^ Cumming-Bruce, Nick (2013-06-04). "U.N. Panel Reports Increasing Brutality by Both Sides in Syria". The New York Times. +Jump up ^ Richard J. Grunawalt. Hospital Ships In The War On Terror: Sanctuaries or Targets? (PDF), Naval War College Review, Winter 2005, pp. 110–11. +Jump up ^ Paul Rogers (2000) "Politics in the Next 50 Years: The Changing Nature of International Conflict" +Jump up ^ J. Gilmore Childers, Henry J. DePippo (February 24, 1998). "Senate Judiciary Committee, Subcommittee on Technology, Terrorism, and Government Information hearing on "Foreign Terrorists in America: Five Years After the World Trade Center"". Fas.org. Retrieved 2011-07-12. +Jump up ^ P. Neuwald, H. Reichenbach, A. L. Kuhl (2003). "Shock-Dispersed-Fuel Charges-Combustion in Chambers and Tunnels". +Jump up ^ David Eshel (2006). "Is the world facing Thermobaric Terrorism?".[dead link] +Jump up ^ Wayne Turnbull (2003). "Bali:Preparations". +External links[edit] +Fuel/Air Explosive (FAE) +Thermobaric Explosive (Global Security) +Aspects of thermobaric weaponry (PDF) – Dr. Anna E Wildegger-Gaissmaier, Australian Defence Force Health +Thermobaric warhead for RPG-7 +XM1060 40 mm Thermobaric Grenade (Global Security) +Defense Update: Fuel-Air Explosive Mine Clearing System +Foreign Military Studies Office – A 'Crushing' Victory: Fuel-Air Explosives and Grozny 2000 +Soon to make a comeback in Afghanistan +Russia claims to have tested the most powerful "Vacuum" weapon +Categories: Explosive weaponsAmmunitionThermobaric weaponsAnti-personnel weapons +Navigation menu +Create accountLog inArticleTalkReadEditView history + +Main page +Contents +Featured content +Current events +Random article +Donate to Wikipedia +Wikimedia Shop +Interaction +Help +About Wikipedia +Community portal +Recent changes +Contact page +Tools +What links here +Related changes +Upload file +Special pages +Permanent link +Page information +Wikidata item +Cite this page +Print/export +Create a book +Download as PDF +Printable version +Languages +العربية +Беларуская +Български +Čeština +Deutsch +Español +فارسی +Français +हिन्दी +Italiano +עברית +Latviešu +Македонски +Nederlands +日本語 +Polski +Русский +Suomi +Svenska +Türkçe +Українська +Tiếng Việt +粵語 +中文 +Edit links +This page was last modified on 28 November 2014 at 10:32. +Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc., a non-profit organization. +Privacy policyAbout WikipediaDisclaimersContact WikipediaDevelopersMobile viewWikimedia Foundation Powered by MediaWiki + + +Gunpowder +From Wikipedia, the free encyclopedia +For other uses, see Gunpowder (disambiguation). +In American English, the term gunpowder also refers broadly to any gun propellant.[1] Gunpowder (black powder) as described in this article is not normally used in modern firearms, which instead use smokeless powders. + +Black powder for muzzleloading rifles and pistols in FFFG granulation size. American Quarter (diameter 24 mm) for comparison. +Gunpowder, also known as black powder, is a chemical explosive—the earliest known. It is a mixture of sulfur, charcoal, and potassium nitrate (saltpeter). The sulfur and charcoal act as fuels, and the saltpeter is an oxidizer.[2][3] Because of its burning properties and the amount of heat and gas volume that it generates, gunpowder has been widely used as a propellant in firearms and as a pyrotechnic composition in fireworks. +Gunpowder is assigned the UN number UN0027 and has a hazard class of 1.1D. It has a flash point of approximately 427–464 °C (801–867 °F). The specific flash point may vary based on the specific composition of the gunpowder. Gunpowder's gravity is 1.70–1.82 (mercury method) orŠ 1.92–2.08 (pycnometer), and it has a pH of 6.0–8.0. It is also considered to be an insoluble material.[4] +Gunpowder was, according to prevailing academic consensus, invented in the 9th century in China,[5][6] and the earliest record of a written formula for gunpowder appears in the 11th century Song Dynasty text, Wujing Zongyao.[7] This discovery led to the invention of fireworks and the earliest gunpowder weapons in China. In the centuries following the Chinese discovery, gunpowder weapons began appearing in the Muslim world, Europe, and India. The technology spread from China through the Middle East or Central Asia, and then into Europe.[8] The earliest Western accounts of gunpowder appear in texts written by English philosopher Roger Bacon in the 13th century.[9] +Gunpowder is classified as a low explosive because of its relatively slow decomposition rate and consequently low brisance. Low explosives deflagrate (i.e., burn) at subsonic speeds, whereas high explosives detonate, producing a supersonic wave. Gunpowder's burning rate increases with pressure, so it bursts containers if contained but otherwise just burns in the open. Ignition of the powder packed behind a bullet must generate enough pressure to force it from the muzzle at high speed, but not enough to rupture the gun barrel. Gunpowder thus makes a good propellant, but is less suitable for shattering rock or fortifications. Gunpowder was widely used to fill artillery shells and in mining and civil engineering to blast rock roughly until the second half of the 19th century, when the first high explosives (nitro-explosives) were discovered. Gunpowder is no longer used in modern explosive military warheads, nor is it used as main explosive in mining operations due to its cost relative to that of newer alternatives such as ammonium nitrate/fuel oil (ANFO).[10] Black powder is still used as a delay element in various munitions where its slow-burning properties are valuable. +Formulations used in blasting rock (such as in quarrying) are called blasting powder. +Contents [hide] +1 History +1.1 China +1.2 Middle East +1.3 Mainland Europe +1.4 Britain and Ireland +1.5 India +1.6 Indonesia +2 Manufacturing technology +3 Composition and characteristics +4 Serpentine +5 Corning +6 Modern types +7 Other types of gunpowder +8 Sulfur-free gunpowder +9 Combustion characteristics +9.1 Advantages +9.2 Disadvantages +9.3 Transportation +10 Other uses +11 See also +12 References +13 External links +History[edit] + +Early Chinese rocket + +A Mongol bomb thrown against a charging Japanese samurai during the Mongol invasions of Japan after founding the Yuan Dynasty, 1281. +Main article: History of gunpowder +Gunpowder was invented in China while taoists attempted to create a potion of immortality. Chinese military forces used gunpowder-based weapons (i.e. rockets, guns, cannons) and explosives (i.e. grenades and different types of bombs) against the Mongols when the Mongols attempted to invade and breach city fortifications on China's northern borders. After the Mongols conquered China and founded the Yuan Dynasty, they used the Chinese gunpowder-based weapons technology in their attempted invasion of Japan; they also used gunpowder to fuel rockets. +The mainstream scholarly consensus is that gunpowder was invented in China, spread through the Middle East, and then into Europe,[8] although there is a dispute over how much the Chinese advancements in gunpowder warfare influenced later advancements in the Middle East and Europe.[11][12] The spread of gunpowder across Asia from China is widely attributed to the Mongols. One of the first examples of Europeans encountering gunpowder and firearms is at the Battle of Mohi in 1241. At this battle the Mongols not only used gunpowder in early Chinese firearms but in the earliest grenades as well. +A major problem confronting the study of the early history of gunpowder is ready access to sources close to the events described. Often enough, the first records potentially describing use of gunpowder in warfare were written several centuries after the fact, and may well have been colored by the contemporary experiences of the chronicler.[13] It is also difficult to accurately translate original alchemy texts, especially medieval Chinese texts that try to explain phenomena through metaphor, into modern scientific language with rigidly defined terminology. The translation difficulty has led to errors or loose interpretations bordering on artistic licence.[14][15] Early writings potentially mentioning gunpowder are sometimes marked by a linguistic process where old words acquired new meanings.[16] For instance, the Arabic word naft transitioned from denoting naphtha to denoting gunpowder, and the Chinese word pao evolved from meaning catapult to referring to cannon.[17] According to science and technology historian Bert S. Hall: "It goes without saying, however, that historians bent on special pleading, or simply with axes of their own to grind, can find rich material in these terminological thickets."[18] +China[edit] +Further information: Wujing Zongyao, Four Great Inventions and List of Chinese inventions + +Chinese Ming Dynasty (1368-1644) matchlock firearms +Saltpeter was known to the Chinese by the mid-1st century AD and there is strong evidence of the use of saltpeter and sulfur in various largely medicinal combinations.[19] A Chinese alchemical text dated 492 noted saltpeter burnt with a purple flame, providing a practical and reliable means of distinguishing it from other inorganic salts, thus enabling alchemists to evaluate and compare purification techniques; the earliest Latin accounts of saltpeter purification are dated after 1200.[20] + +Yuan Dynasty bronze hand cannon from 1332 at th (c. 808); it describes mixing six parts sulfur to six parts saltpeter to one part birthwort herb (which would provide carbon).[21] +The first reference to the incendiary properties of such mixtures is the passage of the Zhenyuan miaodao yaolüe, a Taoist text tentatively dated to the mid-9th century AD:[20] "Some have heated together sulfur, realgar and saltpete with honey; smoke and flames result, so that their hands and faces have been burnt, and even the whole house where they were working burned down."[22] The Chinese word for "gunpowder" is Chinese: 火药/火藥; pinyin: huŏ yào /xuou yɑʊ/, which literally means "Fire Medicine";[23] however this name only came into use some centuries after the mixture's discovery.[24] During the 9th century, Taoist monks or alchemists searching for an elixir of immortality had serendipitously stumbled upon gunpowder.[8][25] The Chinese wasted little time in applying gunpowder to the development of weapons, and in the centuries that followed, they produced a variety of gunpowder weapons, including flamethrowers, rockets, bombs, and land mines, before inventing guns as a projectile weapon.[26] Archaeological evidence of a hand cannon has been excavated in Manchuria dated from the late 1200s[27] and the shells of explosive bombs have been discovered in a shipwreck off the shore of Japan dated from 1281, during the Mongol invasions of Japan.[28] +The Chinese "Wu Ching Tsung Yao" (Complete Essentials from the Military Classics), written by Tseng Kung-Liang between 1040–1044, provides encyclopedia references to a variety of mixtures that included petrochemicals—as well as garlic and honey. A slow match for flame throwing mechanisms using the siphon principle and for fireworks and rockets are mentioned. The mixture formulas in this book do not contain enough saltpeter to create an explosive however; being limited to at most 50% saltpeter, they produce an incendiary.[29] The Essentials was however written by a Song Dynasty court bureaucrat, and there's little evidence that it had any immediate impact on warfare; there is no mention of gunpowder use in the chronicles of the wars against the Tanguts in the eleventh century, and China was otherwise mostly at peace during this century. The first chronicled use of "fire spears" (or "fire lances") is at the siege of De'an in 1132.[30] + +Formula for gunpowder in 1044 Wujing zongyao part I vol 12 + + +Instruction for fire bomb in Wujing zongyao + + +Fire bomb + + +Fire grenade + + +Proto-cannon from the Ming Dynasty text Huolongjing + + +Land mine from the Ming Dynasty text Huolongjing + + +Fire arrow rocket launcher from the Wujing zongyao +Middle East[edit] +Main articles: Inventions in the Islamic world and Alchemy and chemistry in Islam + +The Sultani Cannon, a very heavy bronze breech-loading cannon of type used by Ottoman Empire in the conquest of Constantinople, in 1453. +The Muslims acquired knowledge of gunpowder some time between 1240 and 1280, by which time the Syrian Hasan al-Rammah had written, in Arabic, recipes for gunpowder, instructions for the purification of saltpeter, and descriptions of gunpowder incendiaries. Gunpowder arrived in the Middle East, possibly through India, from China. This is implied by al-Rammah's usage of "terms that suggested he derived his knowledge from Chinese sources" and his references to saltpeter as "Chinese snow" Arabic: ثلج الصين‎ thalj al-ṣīn, fireworks as "Chinese flowers" and rockets as "Chinese arrows".[31] However, because al-Rammah attributes his material to "his father and forefathers", al-Hassan argues that gunpowder became prevalent in Syria and Egypt by "the end of the twelfth century or the beginning of the thirteenth".[32] Persians called saltpeter "Chinese salt" [33][34][35][36][37] or "salt from Chinese salt marshes" (namak shūra chīnī Persian: نمک شوره چيني‎).[38][39] + +A picture of a 15th-century Granadian cannon from the book Al-izz wal rifa'a. +Al-Hassan claims that in the Battle of Ain Jalut of 1260, the Mamluks used against the Mongols in "the first cannon in history" gunpowder formula with near-identical ideal composition ratios for explosive gunpowder.[32] Other historians urge caution regarding claims of Islamic firearms use in the 1204-1324 period as late medieval Arabic texts used the same word for gunpowder, naft, that they used for an earlier incendiary, naphtha.[13][17] Khan claims that it was invading Mongols who introduced gunpowder to the Islamic world[40] and cites Mamluk antagonism towards early musketeers in their infantry as an example of how gunpowder weapons were not always met with open acceptance in the Middle East.[41] Similarly, the refusal of their Qizilbash forces to use firearms contributed to the Safavid rout at Chaldiran in 1514.[41] +The earliest surviving documentary evidence for the use of the hand cannon, considered the oldest type of portable firearm and a forerunner of the handgun, are from several Arabic manuscripts dated to the 14th century.[42] Al-Hassan argues that these are based on earlier originals and that they report hand-held cannons being used by the Mamluks at the Battle of Ain Jalut in 1260.[32] +Hasan al-Rammah included 107 gunpowder recipes in his text al-Furusiyyah wa al-Manasib al-Harbiyya (The Book of Military Horsemanship and Ingenious War Devices), 22 of which are for rockets. If one takes the median of 17 of these 22 compositions for rockets (75% nitrates, 9.06% sulfur, and 15.94% carbon), it is nearly identical to the modern reported ideal gunpowder recipe of 75% potassium nitrate, 10% sulfur, and 15% carbon.[32] +The state-controlled manufacture of gunpowder by the Ottoman Empire through early supply chains to obtain nitre, sulfur and high-quality charcoal from oaks in Anatolia contributed significantly to its expansion the 15th and 18th century. It was not until later in the 19th century when the syndicalist production of Turkish gunpowder was greatly reduced, which coincided with the decline of its military might.[43] +Mainland Europe[edit] +Several sources mention Chinese firearms and gunpowder weapons being deployed by the Mongols against European forces at the Battle of Mohi in 1241.[44][45][46] Professor Kenneth Warren Chase credits the Mongols for introducing into Europe gunpowder and its associated weaponry.[47] +C. F. Temler interprets Peter, Bishop of Leon, as reporting the use of cannons in Seville in 1248.[48] +In Europe, one of the first mentions of gunpowder use appears in a passage found in Roger Bacon's Opus Maius and Opus Tertium in what has been interpreted as being firecrackers. The most telling passage reads: "We have an example of these things (that act on the senses) in [the sound and fire of] that children's toy which is made in many [diverse] parts of the world; i.e., a device no bigger than one's thumb. From the violence of that salt called saltpeter [together with sulfur and willow charcoal, combined into a powder] so horrible a sound is made by the bursting of a thing so small, no more than a bit of parchment [containing it], that we find [the ear assaulted by a noise] exceeding the roar of strong thunder, and a flash brighter than the most brilliant lightning."[9] In the early 20th century, British artillery officer Henry William Lovett Hime proposed that another work tentatively attributed to Bacon, Epistola de Secretis Operibus Artis et Naturae, et de Nullitate Magiae contained an encrypted formula for gunpowder. This claim has been disputed by historians of science including Lynn Thorndike, John Maxson Stillman and George Sarton and by Bacon's editor Robert Steele, both in terms of authenticity of the work, and with respect to the decryption method.[9] In any case, the formula claimed to have been decrypted (7:5:5 saltpeter:charcoal:sulfur) is not useful for firearms use or even firecrackers, burning slowly and producing mostly smoke.[49][50] + +Cannon forged in 1667 at the Fortín de La Galera, Nueva Esparta, Venezuela. +The Liber Ignium, or Book of Fires, attributed to Marcus Graecus, is a collection of incendiary recipes, including some gunpowder recipes. Partington dates the gunpowder recipes to approximately 1300.[51] One recipe for "flying fire" (ingis volatilis) involves saltpeter, sulfur, and colophonium, which, when inserted into a reed or hollow wood, "flies away suddenly and burns up everything." Another recipe, for artificial "thunder", specifies a mixture of one pound native sulfur, two pounds linden or willow charcoal, and six pounds of saltpeter.[52] Another specifies a 1:3:9 ratio.[52] +Some of the gunpowder recipes of De Mirabilibus Mundi of Albertus Magnus are identical to the recipes of the Liber Ignium, and according to Partington, "may have been taken from that work, rather than conversely."[53] Partington suggests that some of the book may have been compiled by Albert's students, "but since it is found in thirteenth century manuscripts, it may well be by Albert."[53] Albertus Magnus died in 1280. +A common German folk-tale is of the German priest/monk named Berthold Schwarz who independently invented gunpowder, thus earning it the German name Schwarzpulver or in English Schwarz's powder. Schwarz is also German for black so this folk-tale, while likely containing elements of truth, is considered problematic. +A major advance in manufacturing began in Europe in the late 14th century when the safety and thoroughness of incorporation was improved by wet grinding; liquid, such as distilled spirits or perhaps the urine of wine-drinking bishops[54] was added during the grinding-together of the ingredients and the moist paste dried afterwards. (The principle of wet mixing to prevent the separation of dry ingredients, invented for gunpowder, is used today in the pharmaceutical industry.[55]) It was also discovered that if the paste was rolled into balls before drying the resulting gunpowder absorbed less water from the air during storage and traveled better. The balls were then crushed in a mortar by the gunner immediately before use, with the old problem of uneven particle size and packing causing unpredictable results. +If the right size particles were chosen, however, the result was a great improvement in power. Forming the damp paste into corn-sized clumps by hand or with the use of a sieve instead of larger balls produced a product after drying that loaded much better, as each tiny piece provided its own surrounding air space that allowed much more rapid combustion than a fine powder. This "corned" gunpowder was from 30% to 300% more powerful. An example is cited where 34 pounds of serpentine was needed to shoot a 47 pound ball, but only 18 pounds of corned powder.[54] The optimum size of the grain depended on its use; larger for large cannon, finer for small arms. Larger cast cannons were easily muzzle-loaded with corned powder using a long-handled ladle. Corned powder also retained the advantage of low moisture absorption, as even tiny grains still had much less surface area to attract water than a floury powder. +During this time, European manufacturers also began regularly purifying saltpeter, using wood ashes containing potassium carbonate to precipitate calcium from their dung liquor, and using ox blood, alum, and slices of turnip to clarify the solution.[54] +Gunpowder-making and metal-smelting and casting for shot and cannon fee was closely held by skilled military tradesmen, who formed guilds that collected dues, tested apprentices, and gave pensions. "Fire workers" were also required to craft fireworks for celebrations of victory or peace. During the Renaissance, two European schools of pyrotechnic thought emerged, one in Italy and the other at Nuremberg, Germany. Vannoccio Biringuccio, born in 1480, was a member of the guild Fraternita di Santa Barbara but broke with the tradition of secrecy by setting down everything he knew in a book titled De la pirotechnia, written in vernacular. The first printed book on either gunpowder or metalworking, it was published posthumously in 1540, with 9 editions over 138 years, and also reprinted by MIT Press in 1966.[54] By the mid-17th century fireworks were used for entertainment on an unprecedented scale in Europe, being popular even at resorts and public gardens.[56] +In 1774 Louis XVI ascended to the throne of France at age 20. After he discovered that France was not self-sufficient in gunpowder, a Gunpowder Administration was established; to head it, the lawyer Antoine Lavoisier was appointed. Although from a bourgeois family, after his degree in law Lavoisier became wealthy from a company set up to collect taxes for the Crown; this allowed him to pursue experimental natural science as a hobby.[57] +Without access to cheap Indian saltpeter (controlled by the British), for hundreds of years France had relied on saltpetermen with royal warrants, the droit de fouille or "right to dig", to seize nitrous-containing soil and demolished walls of barnyards, without compensation to the owners.[58] This caused farmers, the wealthy, or entire villages to bribe the petermen and the associated bureaucracy to leave their buildings alone and the saltpeter uncollected. Lavoisier instituted a crash program to increase saltpeter production, revised (and later eliminated) the droit de fouille, researched best refining and powder manufacturing methods, instituted management and record-keeping, and established pricing that encouraged private investment in works. Although saltpeter from new Prussian-style putrefaction works had not been produced yet (the process taking about 18 months), in only a year France had gunpowder to export. A chief beneficiary of this surplus was the American Revolution. By careful testing and adjusting the proportions and grinding time, powder from mills such as at Essonne outside Paris became the best in the world by 1788, and inexpensive.[58] [59] +Britain and Ireland[edit] + +The old Powder or Pouther magazine dating from 1642, built by order of Charles I. Irvine, North Ayrshire, Scotland +Gunpowder production in Britain appears to have started in the mid 14th century AD with the aim of supplying the English Crown.[60] Records show that gunpowder was being made, in England, in 1346, at the Tower of London; a powder house existed at the Tower in 1461; and in 1515 three King's gunpowder makers worked there.[60] Gunpowder was also being made or stored at other Royal castles, such as Portchester. By the early 14th century, according to N.J.G. Pounds's study The Medieval Castle in England and Wales, many English castles had been deserted and others were crumbling. Their military significance faded except on the borders. Gunpowder had made smaller castles useless.[61] +Henry VIII of England was short of gunpowder when he invaded France in 1544 and England needed to import gunpowder via the port of Antwerp in what is now Belgium.[60] +The English Civil War (1642–1645) led to an expansion of the gunpowder industry, with the repeal of the Royal Patent in August 1641.[60] +Two British physicists, Andrew Noble and Frederick Abel, worked to improve the properties of black powder during the late 19th century. This formed the basis for the Noble-Abel gas equation for internal ballistics.[62] +The introduction of smokeless powder in the late 19th century led to a contraction of the gunpowder industry. After the end of World War I, the majority of the United Kingdom gunpowder manufacturers merged into a single company, "Explosives Trades limited"; and number of sites were closed down, including those in Ireland. This company became Nobel Industries Limited; and in 1926 became a founding member of Imperial Chemical Industries. The Home Office removed gunpowder from its list of Permitted Explosives; and shortly afterwards, on 31 December 1931, the former Curtis & Harvey's Glynneath gunpowder factory at Pontneddfechan, in Wales, closed down, and it was demolished by fire in 1932.[63] + +Gunpowder storing barrels at Martello tower in Point Pleasant Park +The last remaining gunpowder mill at the Royal Gunpowder Factory, Waltham Abbey was damaged by a German parachute mine in 1941 and it never reopened.[64] This was followed by the closure of the gunpowder section at the Royal Ordnance Factory, ROF Chorley, the section was closed and demolished at the end of World War II; and ICI Nobel's Roslin gunpowder factory, which closed in 1954.[64][65] +This left the sole United Kingdom gunpowder factory at ICI Nobel's Ardeer site in Scotland; it too closed in October 1976.[64] Since then gunpowder has been imported into the United Kingdom. In the late 1970s/early 1980s gunpowder was bought from eastern Europe, particularly from what was then the German Democratic Republic and former Yugoslavia. +India[edit] + +In the year 1780 the British began to annex the territories of the Sultanate of Mysore, during the Second Anglo-Mysore War. The British battalion was defeated during the Battle of Guntur, by the forces of Hyder Ali, who effectively utilized Mysorean rockets and Rocket artillery against the closely massed British forces. + +Mughal Emperor Shah Jahan, hunting deer using a Matchlock as the sun sets in the horizon. +Gunpowder and gunpowder weapons were transmitted to India through the Mongol invasions of India.[66][67] The Mongols were defeated by Alauddin Khilji of the Delhi Sultanate, and some of the Mongol soldiers remained in northern India after their conversion to Islam.[67] It was written in the Tarikh-i Firishta (1606–1607) that Nasir ud din Mahmud the ruler of the Delhi Sultanate presented the envoy of the Mongol ruler Hulegu Khan with a dazzling pyrotechnics display upon his arrival in Delhi in 1258 AD. Nasir ud din Mahmud tried to express his strength as a ruler and tried to ward off any Mongol attempt similar to the Siege of Baghdad (1258).[68] Firearms known as top-o-tufak also existed in many Muslim kingdoms in India by as early as 1366 AD.[68] From then on the employment of gunpowder warfare in India was prevalent, with events such as the "Siege of Belgaum" in 1473 by Sultan Muhammad Shah Bahmani.[69] +The shipwrecked Ottoman Admiral Seydi Ali Reis is known to have introduced the earliest type of Matchlock weapons, which the Ottomans used against the Portuguese during the Siege of Diu (1531). After that, a diverse variety of firearms; large guns in particular, became visible in Tanjore, Dacca, Bijapur, and Murshidabad.[70] Guns made of bronze were recovered from Calicut (1504)- the former capital of the Zamorins[71] +The Mughal Emperor Akbar mass-produced matchlocks for the Mughal Army. Akbar is personally known to have shot a leading Rajput commander during the Siege of Chittorgarh.[72] The Mughals began to use Bamboo rockets (mainly for signalling) and employ Sappers: special units that undermined heavy stone fortifications to plant gunpowder charges. +The Mughal Emperor Shah Jahan is known to have introduced much more advanced Matchlocks, their designs were a combination of Ottoman and Mughal designs. Shah Jahan also countered the British and other Europeans in his province of Gujarāt, which supplied Europe saltpeter for use in gunpowder warfare during the 17th century.[73] Bengal and Mālwa participated in saltpeter production.[73] The Dutch, French, Portuguese, and English used Chhapra as a center of saltpeter refining.[73] +Ever since the founding of the Sultanate of Mysore by Hyder Ali, French military officers were employed to train the Mysore Army. Hyder Ali and his son Tipu Sultan were the first to introduce modern Cannons and Muskets, their army was also the first in India to have official uniforms. During the Second Anglo-Mysore War Hyder Ali and his son Tipu Sultan unleashed the Mysorean rockets at their British opponents effectively defeating them on various occasions. The Mysorean rockets inspired the development of the Congreve rocket, which the British widely utilized during the Napoleonic Wars and the War of 1812.[74] +Indonesia[edit] +The Javanese Majapahit Empire was arguably able to encompass much of modern day Indonesia due to its unique mastery of bronze smithing and use of a central arsenal fed by a large number of cottage industries within the immediate region. Documentary and archeological evidence indicate that Arab or Indian traders introduced gunpowder, gonnes, muskets, blunderbusses, and cannons to the Javanese, Acehnese, and Batak via long established commercial trade routes around the early to mid 14th century CE.[75] Portuguese and Spanish invaders were unpleasantly surprised and occasionally even outgunned on occasion.[76] The resurgent Singhasari Empire overtook Sriwijaya and later emerged as the Majapahit whose warfare featured the use of fire-arms and cannonade.[77] Circa 1540 CE the Javanese, always alert for new weapons found the newly arrived Portuguese weaponry superior to that of the locally made variants. Javanese bronze breech-loaded swivel-guns, known as meriam, or erroneously as lantaka, was used widely by the Majapahit navy as well as by pirates and rival lords. The demise of the Majapahit empire and the dispersal of disaffected skilled bronze cannon-smiths to Brunei, modern Sumatra, Malaysia and the Philippines lead to widespread use, especially in the Makassar Strait. +Saltpeter harvesting was recorded by Dutch and German travelers as being common in even the smallest villages and was collected from the decomposition process of large dung hills specifically piled for the purpose. The Dutch punishment for possession of non-permitted gunpowder appears to have been amputation.[78] Ownership and manufacture of gunpowder was later prohibited by the colonial Dutch occupiers.[75] According to a colonel McKenzie quoted in Sir Thomas Stamford Raffles, The History of Java (1817), the purest sulfur was supplied from a crater from a mountain near the straits of Bali.[77] +Manufacturing technology[edit] + +Edge-runner mill in a restored mill, at Eleutherian Mills +For the most powerful black powder meal, a wood charcoal is used. The best wood for the purpose is Pacific willow,[79] but others such as alder or buckthorn can be used. In Great Britain between the 15th to 19th centuries charcoal from alder buckthorn was greatly prized for gunpowder manufacture; cottonwood was used by the American Confederate States.[80] The ingredients are reduced in particle size and mixed as intimately as possible. Originally this was with a mortar-and-pestle or a similarly operating stamping-mill, using copper, bronze or other non-sparking materials, until supplanted by the rotating ball mill principle with non-sparking bronze or lead. Historically, a marble or limestone edge runner mill, running on a limestone bed was used in Great Britain; however, by the mid 19th century AD this had changed to either an iron shod stone wheel or a cast iron wheel running on an iron bed.[81] The mix was dampened with alcohol or water during grinding to prevent accidental ignition. This also helps the extremely soluble saltpeter mix into the microscopic nooks and crannies of the very high surface-area charcoal. +Around the late 14th century AD, European powdermakers first began adding liquid during grinding to improve mixing, reduce dust, and with it the risk of explosion.[82] The powder-makers would then shape the resulting paste of dampened gunpowder, known as mill cake, into corns, or grains, to dry. Not only did corned powder keep better because of its reduced surface area, gunners also found that it was more powerful and easier to load into guns. Before long, powder-makers standardized the process by forcing mill cake through sieves instead of corning powder by hand. +The improvement was based on reducing the surface area of a higher density composition. At the beginning of the 19th century, makers increased density further by static pressing. They shoveled damp mill cake into a two-foot square box, placed this beneath a screw press and reduced it to 1/2 its volume. "Presscake" had the hardness of slate. They broke the dried slabs with hammers or rollers, and sorted the granules with sieves into different grades. In the United States, Irenee du Pont, who had learned the trade from Lavoisier, tumbled the dried grains in rotating barrels to round the edges and increase durability during shipping and handling. (Sharp grains rounded off in transport, producing fine "meal dust" that changed the burning properties.) +Another advance was the manufacture of kiln charcoal by distilling wood in heated iron retorts instead of burning it in earthen pits. Controlling the temperature influenced the power and consistency of the finished gunpowder. In 1863, in response to high prices for Indian saltpeter, DuPont chemists developed a process using potash or mined potassium chloride to convert plentiful Chilean sodium nitrate to potassium nitrate.[83] +During the 18th century gunpowder factories became increasingly dependent on mechanical energy.[84] Despite mechanization, production difficulties related to humidity control, especially during the pressing, were still present in the late 19th century. A paper from 1885 laments that "Gunpowder is such a nervous and sensitive spirit, that in almost every process of manufacture it changes under our hands as the weather changes." Pressing times to the desired density could vary by factor of three depending on the atmospheric humidity.[85] +Composition and characteristics[edit] +The term black powder was coined in the late 19th century, primarily in the United States, to distinguish prior gunpowder formulations from the new smokeless powders and semi-smokeless powders, in cases where these are not referred to as cordite. Semi-smokeless powders featured bulk volume properties that approximated black powder, but had significantly reduced amounts of smoke and combustion products. Smokeless powder has different burning properties (pressure vs. time) and can generate higher pressures and work per gram. This can rupture older weapons designed for black powder. Smokeless powders ranged in color from brownish tan to yellow to white. Most of the bulk semi-smokeless powders ceased to be manufactured in the 1920s.[86][87][88] +Black powder is a granular mixture of +a nitrate, typically potassium nitrate (KNO3), which supplies oxygen for the reaction; +charcoal, which provides carbon and other fuel for the reaction, simplified as carbon (C); +sulfur (S), which, while also serving as a fuel, lowers the temperature required to ignite the mixture, thereby increasing the rate of combustion. +Potassium nitrate is the most important ingredient in terms of both bulk and function because the combustion process releases oxygen from the potassium nitrate, promoting the rapid burning of the other ingredients.[89] To reduce the likelihood of accidental ignition by static electricity, the granules of modern black powder are typically coated with graphite, which prevents the build-up of electrostatic charge. +Charcoal does not consist of pure carbon; rather, it consists of partially pyrolyzed cellulose, in which the wood is not completely decomposed. Carbon differs from charcoal. Whereas charcoal's autoignition temperature is relatively low, carbon's is much greater. Thus, a black powder composition containing pure carbon would burn similarly to a match head, at best.[90] +The current standard composition for the black powders that are manufactured by pyrotechnicians was adopted as long ago as 1780. Proportions by weight are 75% potassium nitrate (known as saltpeter or saltpetre), 15% softwood charcoal, and 10% sulfur.[81] These ratios have varied over the centuries and by country, and can be altered somewhat depending on the purpose of the powder. For instance, power grades of black powder, unsuitable for use in firearms but adequate for blasting rock in quarrying operations, is called blasting powder rather than gunpowder with standard proportions of 70% nitrate, 14% charcoal, and 16% sulfur; blasting powder may be made with the cheaper sodium nitrate substituted for potassium nitrate and proportions may be as low as 40% nitrate, 30% charcoal, and 30% sulfur.[91] In 1857, Lamont DuPont solved the main problem of using cheaper sodium nitrate formulations when he patented DuPont "B" Blasting powder. After manufacturing grains from press-cake in the usual way, his process tumbled the powder with graphite dust for 12 hours. This formed a graphite coating on each grain that reduced its ability to absorb moisture.[92] +French war powder in 1879 used the ratio 75% saltpeter, 12.5% charcoal, 12.5% sulfur. English war powder in 1879 used the ratio 75% saltpeter, 15% charcoal, 10% sulfur.[93] The British Congreve rockets used 62.4% saltpeter, 23.2% charcoal and 14.4% sulfur, but the British Mark VII gunpowder was changed to 65% saltpeter, 20% charcoal and 15% sulfur.[94] The explanation for the wide variety in formulation relates to usage. Powder used for rocketry can use a slower burn rate since it accelerates the projectile for a much longer time—whereas powders for weapons such as flintlocks, cap-locks, or matchlocks need a higher burn rate to accelerate the projectile in a much shorter distance. Cannons usually used lower burn rate powders, because most would burst with higher burn rate powders. +Serpentine[edit] +The original dry-compounded powder used in fifteenth-century Europe was known as "Serpentine", either a reference to Satan[95] or to a common artillery piece that used it.[96] The ingredients were ground together with a mortar and pestle, perhaps for 24 hours,[96] resulting in a fine flour. Vibration during transportation could cause the components to separate again, requiring remixing in the field. Also if the quality of the saltpeter was low (for instance if it was contaminated with highly hygroscopic calcium nitrate), or if the powder was simply old (due to the mildly hygroscopic nature of potassium nitrate), in humid weather it would need to be re-dried. The dust from "repairing" powder in the field was a major hazard. +Loading cannons or bombards before the powder-making advances of the Renaissance was a skilled art. Fine powder loaded haphazardly or too tightly would burn incompletely or too slowly. Typically, the breech-loading powder chamber in the rear of the piece was filled only about half full, the serpentine powder neither too compressed nor too loose, a wooden bung pounded in to seal the chamber from the barrel when assembled, and the projectile placed on. A carefully determined empty space was necessary for the charge to burn effectively. When the cannon was fired through the touchhole, turbulence from the initial surface combustion caused the rest of the powder to be rapidly exposed to the flame.[96] +The advent of much more powerful and easy to use corned powder changed this procedure, but serpentine was used with older guns into the seventeenth century.[97] +Corning[edit] +For gunpowder to explode effectively, the combustible ingredients must be reduced to the smallest possible particle sizes, and thoroughly mixed as possible. Once mixed, however, for better results in a gun, makers discovered that the final product should be in the form of individual, dense, grains that spread the fire quickly from grain to grain, much as straw or twigs catch fire more quickly than a pile of sawdust. +Primarily for safety reasons, size reduction and mixing is done while the ingredients are damp, usually with water. After 1800, instead of forming grains by hand or with sieves, the damp mill-cake was pressed in molds to increase its density and extract the liquid, forming press-cake. The pressing took varying amounts of time, depending on conditions such as atmospheric humidity. The hard, dense product was broken again into tiny pieces, which were separated with sieves to produce a uniform product for each purpose: coarse powders for cannons, finer grained powders for muskets, and the finest for small hand guns and priming.[97] Inappropriately fine-grained powder often caused cannons to burst before the projectile could move down the barrel, due to the high initial spike in pressure.[98] Mammoth powder with large grains made for Rodman's 15-inch cannon reduced the pressure to only 20 percent as high as ordinary cannon powder would have produced.[99] +In the mid-nineteenth century, measurements were made determining that the burning rate within a grain of black powder (or a tightly packed mass) is about 0.20 fps, while the rate of ignition propagation from grain to grain is around 30 fps, over two orders of magnitude faster.[97] +Modern types[edit] +Modern corning first compresses the fine black powder meal into blocks with a fixed density (1.7 g/cm³).[100] In the United States, gunpowder grains were designated F (for fine) or C (for coarse). Grain diameter decreased with a larger number of Fs and increased with a larger number of Cs, ranging from about 2 mm for 7F to 15 mm for 7C. Even larger grains were produced for artillery bore diameters greater than about 17 cm (6.7 in). The standard DuPont Mammoth powder developed by Thomas Rodman and Lammot du Pont for use during the American Civil War had grains averaging 0.6 inches diameter, with edges rounded in a glazing barrel.[99] Other versions had grains the size of golf and tennis balls for use in 20-inch (50-cm) Rodman guns.[101] In 1875 DuPont introduced Hexagonal powder for large artillery, which was pressed using shaped plates with a small center core—about 1.5 inches diameter, like a wagon wheel nut, the center hole widened as the grain burned.[102] By 1882 German makers also produced hexagonal grained powders of a similar size for artillery.[102] +By the late 19th century manufacturing focused on standard grades of black powder from Fg used in large bore rifles and shotguns, through FFg (medium and small-bore arms such as muskets and fusils), FFFg (small-bore rifles and pistols), and FFFFg (extreme small bore, short pistols and most commonly for priming flintlocks).[103] A coarser grade for use in military artillery blanks was designated A-1. These grades were sorted on a system of screens with oversize retained on a mesh of 6 wires per inch, A-1 retained on 10 wires per inch, Fg retained on 14, FFg on 24, FFFg on 46, and FFFFg on 60. Fines designated FFFFFg were usually reprocessed to minimize explosive dust hazards.[104] In the United Kingdom, the main service gunpowders were classified RFG (rifle grained fine) with diameter of one or two millimeters and RLG (rifle grained large) for grain diameters between two and six millimeters.[101] Gunpowder grains can alternatively be categorized by mesh size: the BSS sieve mesh size, being the smallest mesh size, which retains no grains. Recognized grain sizes are Gunpowder G 7, G 20, G 40, and G 90. +Owing to the large market of antique and replica black-powder firearms in the US, modern gunpowder substitutes like Pyrodex, Triple Seven and Black Mag3[105] pellets have been developed since the 1970s. These products, which should not be confused with smokeless powders, aim to produce less fouling (solid residue), while maintaining the traditional volumetric measurement system for charges. Claims of less corrosiveness of these products have been controversial however. New cleaning products for black-powder guns have also been developed for this market.[103] +Other types of gunpowder[edit] +Besides black powder, there are other historically important types of gunpowder. "Brown gunpowder" is cited as composed of 79% nitre, 3% sulfur, and 18% charcoal per 100 of dry powder, with about 2% moisture. Prismatic Brown Powder is a large-grained product the Rottweil Company introduced in 1884 in Germany, which was adopted by the British Royal Navy shortly thereafter. The French navy adopted a fine, 3.1 millimeter, not prismatic grained product called Slow Burning Cocoa (SBC) or "cocoa powder". These brown powders reduced burning rate even further by using as little as 2 percent sulfur and using charcoal made from rye straw that had not been completely charred, hence the brown color.[102] +Lesmok powder was a product developed by DuPont in 1911[106] one of several semi-smokeless products in the industry containing a mixture of black and nitrocellulose powder. It was sold to Winchester and others primarily for .22 and .32 small calibers. Its advantage was that it was believed at the time to be less corrosive than smokeless powders then in use. It was not understood in the U.S. until the 1920s that the actual source of corrosion was the potassium chloride residue from potassium chlorate sensitized primers. The bulkier black powder fouling better disperses primer residue. Failure to mitigate primer corrosion by dispersion caused the false impression that nitrocellulose-based powder caused corrosion.[107] Lesmok had some of the bulk of black powder for dispersing primer residue, but somewhat less total bulk than straight black powder, thus requiring less frequent bore cleaning.[108] It was last sold by Winchester in 1947. +Sulfur-free gunpowder[edit] + +Burst barrel of a muzzle loader pistol replica, which was loaded with nitrocellulose powder instead of black powder and couldn't withstand the higher pressures of the modern propellant +The development of smokeless powders, such as cordite, in the late 19th century created the need for a spark-sensitive priming charge, such as gunpowder. However, the sulfur content of traditional gunpowders caused corrosion problems with Cordite Mk I and this led to the introduction of a range of sulfur-free gunpowders, of varying grain sizes.[64] They typically contain 70.5 parts of saltpeter and 29.5 parts of charcoal.[64] Like black powder, they were produced in different grain sizes. In the United Kingdom, the finest grain was known as sulfur-free mealed powder (SMP). Coarser grains were numbered as sulfur-free gunpowder (SFG n): 'SFG 12', 'SFG 20', 'SFG 40' and 'SFG 90', for example; where the number represents the smallest BSS sieve mesh size, which retained no grains. +Sulfur's main role in gunpowder is to decrease the ignition temperature. A sample reaction for sulfur-free gunpowder would be +6 KNO3 + C7H4O → 3 K2CO3 + 4 CO2 + 2 H2O + 3 N2 +Combustion characteristics[edit] +A simple, commonly cited, chemical equation for the combustion of black powder is +2 KNO3 + S + 3 C → K2S + N2 + 3 CO2. +A balanced, but still simplified, equation is[109] +10 KNO3 + 3 S + 8 C → 2 K2CO3 + 3 K2SO4 + 6 CO2 + 5 N2. +Although charcoal's chemical formula varies, it can be best summed up by its empirical formula: C7H4O. +Therefore, an even more accurate equation of the decomposition of regular black powder with the use of sulfur can be described as: +6 KNO3 + C7H4O + 2 S → K2CO3 + K2SO4 + K2S + 4 CO2 + 2 CO + 2 H2O + 3 N2 +Black powder without the use of sulfur: +10 KNO3 + 2 C7H4O → 5 K2CO3 + 4 CO2 + 5 CO + 4 H2O + 5 N2 +The burning of gunpowder does not take place as a single reaction, however, and the byproducts are not easily predicted. One study's results showed that it produced (in order of descending quantities) 55.91% solid products: potassium carbonate, potassium sulfate, potassium sulfide, sulfur, potassium nitrate, potassium thiocyanate, carbon, ammonium carbonate and 42.98% gaseous products: carbon dioxide, nitrogen, carbon monoxide, hydrogen sulfide, hydrogen, methane, 1.11% water. +Black powder made with less-expensive and more plentiful sodium nitrate (in appropriate proportions) works just as well but is more hygroscopic than powders made from Potassium nitrate—popularly known as saltpeter. Because corned black powder grains made with saltpeter are less affected by moisture in the air, they can be stored unsealed without degradation by humidity. Muzzleloaders have been known to fire after hanging on a wall for decades in a loaded state, provided they remained dry. By contrast, black powder made with sodium nitrate must be kept sealed to remain stable. +Gunpowder contains 3 megajoules per kilogram, and contains its own oxidant. For comparison, the energy density of TNT is 4.7 megajoules per kilogram, and the energy density of gasoline is 47.2 megajoules per kilogram. Gunpowder is a low explosive and as such it does not detonate; rather it deflagrates. Since it contains its own oxidizer and additionally burns faster under pressure, its combustion is capable of rupturing containers such as shell, grenade, or improvised "pipe bomb" or "pressure cooker" casings, forming shrapnel. +Advantages[edit] +In quarrying, high explosives are generally preferred for shattering rock. However, because of its low brisance, black powder causes fewer fractures and results in more usable stone compared to other explosives, making black powder useful for blasting monumental stone such as granite and marble. Black powder is well suited for blank rounds, signal flares, burst charges, and rescue-line launches. Black powder is also used in fireworks for lifting shells, in rockets as fuel, and in certain special effects. +Disadvantages[edit] +Black powder has a low energy density compared to modern "smokeless" powders, and thus to achieve high energy loadings, large amounts of black powder are needed with heavy projectiles. Black powder also produces thick smoke as a byproduct, which in military applications may give a soldier's location away to an enemy observer and may also impair aiming for additional shots. +Combustion converts less than half the mass of black powder to gas. The rest ends up as a thick layer of soot inside the barrel. In addition to being a nuisance, the residue from burnt black powder is hygroscopic and with the addition of moisture absorbed from the air, this residue forms a caustic substance. The soot contains potassium oxide or sodium oxide that turns into potassium hydroxide, or sodium hydroxide, which corrodes wrought iron or steel gun barrels. Black powder arms must be well cleaned both inside and out to remove the residue. The matchlock musket or pistol (an early gun ignition system), as well as the flintlock would often be unusable in wet weather, due to powder in the pan being exposed and dampened. Because of this unreliability, soldiers carrying muskets, known as musketeers, were armed with additional weapons such as swords or pikes. The bayonet was developed to allow the musket to be used as a pike, thus eliminating the need for the soldier to carry a secondary weapon. +Transportation[edit] +The United Nations Model Regulations on the Transportation of Dangerous Goods and national transportation authorities, such as United States Department of Transportation, have classified gunpowder (black powder) as a Group A: Primary explosive substance for shipment because it ignites so easily. Complete manufactured devices containing black powder are usually classified as Group D: Secondary detonating substance, or black powder, or article containing secondary detonating substance, such as firework, class D model rocket engine, etc., for shipment because they are harder to ignite than loose powder. As explosives, they all fall into the category of Class 1. +Other uses[edit] +Besides its use as an explosive, gunpowder has been occasionally employed for other purposes; after the Battle of Aspern-Essling (1809), the surgeon of the Napoleonic Army Larrey combated the lack of food for the wounded under his care by preparing a bouillon of horse meat seasoned with gunpowder for lack of salt.[110][111] It was also used for sterilizing on ships when there was no alcohol. +Jack Tars (British sailors) used gunpowder to create tattoos when ink wasn't available, by pricking the skin and rubbing the powder into the wound in a method known as traumatic tattooing.[112] +Christiaan Huygens experimented with gunpowder in 1673 in an early attempt to build an internal combustion engine, but he did not succeed. Modern attempts to recreate his invention were similarly unsuccessful. +Fireworks use gunpowder as lifting and burst charges, although sometimes other more powerful compositions are added to the burst charge to improve performance in small shells or provide a louder report. Most modern firecrackers no longer contain black powder. +Beginning in the 1930s, gunpowder or smokeless powder was used in rivet guns, stun guns for animals, cable splicers and other industrial construction tools.[113] The "stud gun" drove nails or screws into solid concrete, a function not possible with hydraulic tools. See Powder-actuated tool. Shotguns have been used to eliminate persistent material rings in operating rotary kilns (such as those for cement, lime, phosphate, etc.) and clinker in operating furnaces, and commercial tools make the method more reliable.[114] +Near London in 1853, Captain Shrapnel demonstrated a method for crushing gold-bearing ores by firing them from a cannon into an iron chamber, and "much satisfaction was expressed by all present". He hoped it would be useful on the goldfields of California and Australia. Nothing came of the invention, as continuously-operating crushing machines that achieved more reliable comminution were already coming into use.[115] +See also[edit] +Ballistics +Black powder substitute +Faversham explosives industry +Bulk loaded liquid propellants +Gunpowder magazine +Gunpowder Plot +Berthold Schwarz +Gunpowder warfare +History of gunpowder +Technology of the Song Dynasty +References[edit] +Jump up ^ http://www.merriam-webster.com/dictionary/gunpowder +Jump up ^ Jai Prakash Agrawal (2010). High Energy Materials: Propellants, Explosives and Pyrotechnics. Wiley-VCH. p. 69. ISBN 978-3-527-32610-5. +Jump up ^ David Cressy, Saltpeter: The Mother of Gunpowder (Oxford University Press, 2013) +Jump up ^ Owen Compliance Services. "Black Powder". Material Safety Data Sheet. Retrieved 31 August 2014. +Jump up ^ http://www.history.com/shows/ancient-discoveries/articles/who-built-it-first-2 +Jump up ^ http://chemistry.about.com/od/historyofchemistry/a/gunpowder.htm +Jump up ^ Chase 2003:31 : "the earliest surviving formulas for gunpowder can be found in the Wujing zongyao, a military work from around 1040" +^ Jump up to: a b c Buchanan 2006, p. 2 "With its ninth century AD origins in China, the knowledge of gunpowder emerged from the search by alchemists for the secrets of life, to filter through the channels of Middle Eastern culture, and take root in Europe with consequences that form the context of the studies in this volume." +^ Jump up to: a b c Joseph Needham; Gwei-Djen Lu; Ling Wang (1987). Science and civilisation in China, Volume 5, Part 7. Cambridge University Press. pp. 48–50. ISBN 978-0-521-30358-3. +Jump up ^ Hazel Rossotti (2002). Fire: Servant, Scourge, and Enigma. Courier Dover Publications. pp. 132–137. ISBN 978-0-486-42261-9. +Jump up ^ Jack Kelly Gunpowder: Alchemy, Bombards, and Pyrotechnics: The History of the Explosive that Changed the World, Perseus Books Group: 2005, ISBN 0-465-03722-4, ISBN 978-0-465-03722-3: 272 pages +Jump up ^ St. C. Easton: "Roger Bacon and his Search for a Universal Science", Oxford (1962) +^ Jump up to: a b Gábor Ágoston (2005). Guns for the sultan: military power and the weapons industry in the Ottoman Empire. Cambridge University Press. p. 15. ISBN 978-0-521-84313-3. +Jump up ^ Ingham-Brown, George (1989) The Big Bang: A History of Explosives, Sutton Publishers, ISBN 0-7509-1878-0, ISBN 978-0-7509-1878-7, page vi +Jump up ^ Kelly, Jack (2005) Gunpowder: Alchemy, Bombards, and Pyrotechnics: The History of the Explosive that Changed the World, Perseus Books Group, ISBN 0-465-03722-4, ISBN 978-0-465-03722-3, page 22 +Jump up ^ Bert S. Hall, "Introduction, 1999" pp. xvi–xvii to the reprinting of James Riddick Partington (1960). A history of Greek fire and gunpowder. JHU Press. ISBN 978-0-8018-5954-0. +^ Jump up to: a b Peter Purton (2009). A History of the Late Medieval Siege, 1200–1500. Boydell & Brewer. pp. 108–109. ISBN 978-1-84383-449-6. +Jump up ^ Bert S. Hall, "Introduction, 1999" p. xvii to the reprinting of James Riddick Partington (1960). A history of Greek fire and gunpowder. JHU Press. ISBN 978-0-8018-5954-0. +Jump up ^ Buchanan. "Editor's Introduction: Setting the Context", in Buchanan 2006. +^ Jump up to: a b Chase 2003:31–32 +Jump up ^ Lorge, Peter A. (2008). The Asian military revolution, 1300-2000 : from gunpowder to the bomb (1. publ. ed.). Cambridge: Cambridge University Press. p. 32. ISBN 978052160954-8. +Jump up ^ Kelly 2004:4 +Jump up ^ The Big Book of Trivia Fun, Kidsbooks, 2004 +Jump up ^ Peter Allan Lorge (2008), The Asian military revolution: from gunpowder to the bomb, Cambridge University Press, p. 18, ISBN 978-0-521-60954-8 +Jump up ^ Needham 1986, p. 7 "Without doubt it was in the previous century, around +850, that the early alchemical experiments on the constituents of gunpowder, with its self-contained oxygen, reached their climax in the appearance of the mixture itself." +Jump up ^ Chase 2003:1 "The earliest known formula for gunpowder can be found in a Chinese work dating probably from the 800s. The Chinese wasted little time in applying it to warfare, and they produced a variety of gunpowder weapons, including flamethrowers, rockets, bombs, and land mines, before inventing firearms." +Jump up ^ Chase 2003:1 +Jump up ^ Delgado, James (February 2003). "Relics of the Kamikaze". Archaeology (Archaeological Institute of America) 56 (1). +Jump up ^ Chase 2003:31 +Jump up ^ Peter Allan Lorge (2008), The Asian military revolution: from gunpowder to the bomb, Cambridge University Press, pp. 33–34, ISBN 978-0-521-60954-8 +Jump up ^ Kelly 2004:22 'Around year 1240, Arabs acquired knowledge of saltpeter ("Chinese snow") from the East, perhaps through India. They knew of gunpowder soon afterward. They also learned about fireworks ("Chinese flowers") and rockets ("Chinese arrows"). Arab warriors had acquired fire lances before year 1280. Around that same year, a Syrian named Hasan al-Rammah wrote a book that, as he put it, "treats of machines of fire to be used for amusement or for useful purposes." He talked of rockets, fireworks, fire lances, and other incendiaries, using terms that suggested he derived his knowledge from Chinese sources. He gave instructions for the purification of saltpeter and recipes for making different types of gunpowder.' +^ Jump up to: a b c d Hassan, Ahmad Y. "Transfer of Islamic Technology to the West: Part III". History of Science and Technology in Islam. +Jump up ^ Peter Watson (2006). Ideas: A History of Thought and Invention, from Fire to Freud. HarperCollins. p. 304. ISBN 978-0-06-093564-1. The first use of a metal tube in this context was made around 1280 in the wars between the Song and the Mongols, where a new term, chong, was invented to describe the new horror...Like paper, it reached the West via the Muslims, in this case the writings of the Andalusian botanist Ibn al-Baytar, who died in Damascus in 1248. The Arabic term for saltpetre is 'Chinese snow' while the Persian usage is 'Chinese salt'.28 +Jump up ^ Cathal J. Nolan (2006). The age of wars of religion, 1000–1650: an encyclopedia of global warfare and civilization. Volume 1 of Greenwood encyclopedias of modern world wars. Greenwood Publishing Group. p. 365. ISBN 0-313-33733-0. Retrieved 2011-11-28. In either case, there is linguistic evidence of Chinese origins of the technology: in Damascus, Arabs called the saltpeter used in making gunpowder " Chinese snow," while in Iran it was called "Chinese salt." Whatever the migratory route +Jump up ^ Oliver Frederick Gillilan Hogg (1970). Artillery: its origin, heyday, and decline. Archon Books. p. 123. The Chinese were certainly acquainted with saltpetre, the essential ingredient of gunpowder. They called it Chinese Snow and employed it early in the Christian era in the manufacture of fireworks and rockets. +Jump up ^ Oliver Frederick Gillilan Hogg (1963). English artillery, 1326–1716: being the history of artillery in this country prior to the formation of the Royal Regiment of Artillery. Royal Artillery Institution. p. 42. The Chinese were certainly acquainted with saltpetre, the essential ingredient of gunpowder. They called it Chinese Snow and employed it early in the Christian era in the manufacture of fireworks and rockets. +Jump up ^ Oliver Frederick Gillilan Hogg (1993). Clubs to cannon: warfare and weapons before the introduction of gunpowder (reprint ed.). Barnes & Noble Books. p. 216. ISBN 1-56619-364-8. Retrieved 2011-11-28. The Chinese were certainly acquainted with saltpetre, the essential ingredient of gunpowder. They called it Chinese snow and used it early in the Christian era in the manufacture of fireworks and rockets. +Jump up ^ Partington, J. R. (1960). A History of Greek Fire and Gunpowder (illustrated, reprint ed.). JHU Press. p. 335. ISBN 0801859549. Retrieved 2014-11-21. +Jump up ^ Needham, Joseph; Yu, Ping-Yu (1980). Needham, Joseph, ed. Science and Civilisation in China: Volume 5, Chemistry and Chemical Technology, Part 4, Spagyrical Discovery and Invention: Apparatus, Theories and Gifts. Volume 5 (Issue 4 of Science and Civilisation in China). Contributors Joseph Needham, Lu Gwei-Djen, Nathan Sivin (illustrated, reprint ed.). Cambridge University Press. p. 194. ISBN 052108573X. Retrieved 2014-11-21. +Jump up ^ Khan 1996 +^ Jump up to: a b Khan 2004:6 +Jump up ^ Ancient Discoveries, Episode 12: Machines of the East, History Channel, 2007 (Part 4 and Part 5) +Jump up ^ Nelson, Cameron Rubaloff (2010-07). Manufacture and transportation of gunpowder in the Ottoman Empire: 1400-1800 M.A. Thesis. +Jump up ^ William H. McNeill (1992). The Rise of the West: A History of the Human Community. University of Chicago Press. p. 492. ISBN 0-226-56141-0. Retrieved 29 July 2011. +Jump up ^ Michael Kohn (2006), Dateline Mongolia: An American Journalist in Nomad's Land, RDR Books, p. 28, ISBN 1-57143-155-1, retrieved 29 July 2011 +Jump up ^ Robert Cowley (1993). Robert Cowley, ed. Experience of War (reprint ed.). Random House Inc. p. 86. ISBN 0-440-50553-4. Retrieved 29 July 2011. +Jump up ^ Kenneth Warren Chase (2003). Firearms: a global history to 1700 (illustrated ed.). Cambridge University Press. p. 58. ISBN 0-521-82274-2. Retrieved 29 July 2011. +Jump up ^ C. F. Temler, Historische Abhandlungen der Koniglichen Gesellschaft der Wissenschaften zu Kopenhagen ... ubersetzt ... von V. A. Heinze, Kiel, Dresden and Leipzig, 1782, i, 168, as cited in Partington, p. 228, footnote 6. +Jump up ^ Joseph Needham; Gwei-Djen Lu; Ling Wang (1987). Science and civilisation in China, Volume 5, Part 7. Cambridge University Press. p. 358. ISBN 978-0-521-30358-3. +Jump up ^ Bert S. Hall, "Introduction, 1999" p. xxiv to the reprinting of James Riddick Partington (1960). A history of Greek fire and gunpowder. JHU Press. ISBN 978-0-8018-5954-0. +Jump up ^ Partington 1960:60 +^ Jump up to: a b Partington 1960:48–49, 54 +^ Jump up to: a b Partington 1960:82–83 +^ Jump up to: a b c d Kelly 2004, p.61 +Jump up ^ Molerus, Otto. "History of Civilization in the Western Hemisphere from the Point of View of Particulate Technology, Part 2," Advanced Powder Technology 7 (1996): 161-66 +Jump up ^ Microsoft Encarta Online Encyclopedia 2007 Archived 31 October 2009. +Jump up ^ In 1777 Lavoisier named oxygen, which had earlier been isolated by Priestley; the realization that saltpeter contained this substance was fundamental to understanding gunpowder. +^ Jump up to: a b Kelly 2004, p.164 +Jump up ^ Metzner, Paul (1998), Crescendo of the Virtuoso: Spectacle, Skill, and Self-Promotion in Paris during the Age of Revolution, University of California Press +^ Jump up to: a b c d Cocroft 2000, "Success to the Black Art!". Chapter 1 +Jump up ^ Ross, Charles. The Custom of the Castle: From Malory to Macbeth. Berkeley: University of California Press, c1997. [1] pages 131-130 +Jump up ^ The Noble-Abel Equation of State: Thermodynamic Derivations for Ballistics Modelling +Jump up ^ Pritchard, Tom; Evans, Jack; Johnson, Sydney (1985), The Old Gunpowder Factory at Glynneath, Merthyr Tydfil: Merthyr Tydfil & District Naturalists' Society +^ Jump up to: a b c d e Cocroft 2000, "The demise of gunpowder". Chapter 4 +Jump up ^ MacDougall, Ian (2000). 'Oh, ye had to be careful' : personal recollections by Roslin gunpowder mill and bomb factory workers. East Linton, Scotland: Tuckwell Press in association with the European Ethnological Research Centre and the Scottish Working People's History Trust. ISBN 1-86232-126-4. +Jump up ^ Iqtidar Alam Khan (2004). Gunpowder And Firearms: Warfare In Medieval India. Oxford University Press. ISBN 978-0-19-566526-0. +^ Jump up to: a b Iqtidar Alam Khan (25 April 2008). Historical Dictionary of Medieval India. Scarecrow Press. p. 157. ISBN 978-0-8108-5503-8. +^ Jump up to: a b Khan 2004:9–10 +Jump up ^ Khan 2004:10 +Jump up ^ Partington (Johns Hopkins University Press edition, 1999), 225 +Jump up ^ Partington (Johns Hopkins University Press edition, 1999), 226 +Jump up ^ http://www.youtube.com/watch?v=DTfEDaWMj4o +^ Jump up to: a b c "India." Encyclopædia Britannica. Encyclopedia Britannica 2008 Ultimate Reference Suite. Chicago: Encyclopedia Britannica, 2008. +Jump up ^ "rocket and missile system." Encyclopædia Britannica. Encyclopædia Britannica 2008 Ultimate Reference Suite. Chicago: Encyclopædia Britannica, 2008. +^ Jump up to: a b Dipanegara, P. B. R. Carey, Babad Dipanagara: an account of the outbreak of the Java war, 1825-30 : the Surakarta court version of the Babad Dipanagara with translations into English and Indonesian volume 9: Council of the M.B.R.A.S. by Art Printing Works: 1981. +Jump up ^ Atsushi, Ota (2006). Changes of regime and social dynamics in West Java : society, state, and the outer world of Banten, 1750-1830. Leiden: Brill. ISBN 90-04-15091-9. +^ Jump up to: a b Thomas Stamford Raffles, The History of Java, Oxford University Press, 1965 (originally published in 1817), ISBN 0-19-580347-7 +Jump up ^ Raffles, Thomas Stamford (1978). The History of Java ([Repr.]. ed.). Kuala Lumpur: Oxford University Press. ISBN 0-19-580347-7. +Jump up ^ US Department of Agriculture (1917). Department Bulleting No. 316: Willows: Their growth, use, and importance. The Department. p. 31. +Jump up ^ Kelly 2004, p.200 +^ Jump up to: a b Earl 1978, Chapter 2: The Development of Gunpowder +Jump up ^ Kelly 2004:60–63 +Jump up ^ Kelly 2004, p.199 +Jump up ^ Frangsmyr, Tore, J. L. Heilbron, and Robin E. Rider, editors The Quantifying Spirit in the Eighteenth Century. Berkeley: University of California Press, c1990. http://ark.cdlib.org/ark:/13030/ft6d5nb455/ p. 292. +Jump up ^ C.E. Munroe (1885) "Notes on the literature of explosives no. VIII", Proceedings of the US Naval Institute, no. XI, p. 285 +Jump up ^ The History of the 10.4×38 Swiss Cartridge +Jump up ^ Blackpowder to Pyrodex and Beyond by Randy Wakeman at Chuck Hawks +Jump up ^ The History and Art of Shotshells by Jon Farrar, Nebraskaland Magazine +Jump up ^ Buchanan. "Editor's Introduction: Setting the Context", in Buchanan 2006, p. 4. +Jump up ^ Black Powder Recipes, Ulrich Bretscher +Jump up ^ Julian S. Hatcher, Hatcher's Notebook, Military Service Publishing Company, 1947. Chapter XIII Notes on Gunpowder, pages 300-305. +Jump up ^ Kelly 2004, p.218 +Jump up ^ Book title Workshop Receipts Publisher William Clowes and Son limited Author Ernest Spon. Date 1 August 1873. +Jump up ^ GunpowderTranslation. Academic. Retrieved 2014-08-31. +Jump up ^ Cathal J. Nolan (2006), The age of wars of religion, 1000-1650: an encyclopedia of global warfare and civilization, Greenwood Publishing Group, p. 365, ISBN 978-0-313-33733-8 +^ Jump up to: a b c Kelly 2004, p58 +^ Jump up to: a b c John Francis Guilmartin (2003). Gunpowder & galleys: changing technology & Mediterranean warfare at sea in the 16th century. Conway Maritime Press. pp. 109–110 and 298–300. ISBN 0851779514. +Jump up ^ T.J. Rodman (1861), Reports of experiments on the properties of metals for cannon and the qualities of cannon powder, p. 270 +^ Jump up to: a b Kelly 2004, p.195 +Jump up ^ Tenney L. Davis (1943). The Chemistry of Powder and Explosives (PDF). p. 139. +^ Jump up to: a b Brown, G.I. (1998) The Big Bang: A history of Explosives Sutton Publishing pp.22&32 ISBN 0-7509-1878-0 +^ Jump up to: a b c Kelly 2004, p.224 +^ Jump up to: a b Rodney James (2011). The ABCs of Reloading: The Definitive Guide for Novice to Expert (9 ed.). Krause Publications. pp. 53–59. ISBN 978-1-4402-1396-0. +Jump up ^ Sharpe, Philip B. (1953) Complete Guide to Handloading Funk & Wagnalls p.137 +Jump up ^ Wakeman, Randy. "Blackpowder to Pyrodex and Beyond". Retrieved 31 August 2014. +Jump up ^ "LESMOK POWDER". +Jump up ^ Julian S. Hatcher, Hatcher's Notebook, Stackpole Books, 1962. Chapter XIV, Gun Corrosion and Ammunition Developments, pages 346-349. +Jump up ^ Wakeman, Randy. "Blackpowder to Pyrodex and Beyond". +Jump up ^ Flash! Bang! Whiz!, University of Denver +Jump up ^ Parker, Harold T. (1983). Three Napoleonic battles. (Repr., Durham, 1944. ed.). Durham, NC: Duke Univ. Pr. p. 83. ISBN 0-8223-0547-X. +Jump up ^ Larrey is quoted in French at Dr Béraud, Études Hygiéniques de la chair de cheval comme aliment, Musée des Familles (1841-42). +Jump up ^ Rediker, Marcus (1989). Between the devil and the deep blue sea : merchant seamen, pirates, and the Anglo-American maritime world, 1700-1750 (1st pbk. ed. ed.). Cambridge: Cambridge University Press. p. 12. ISBN 9780521379830. +Jump up ^ "Gunpowder Now Used To Drive Rivets And Splice Cables", April 1932, Popular Science +Jump up ^ "MasterBlaster System". Remington Products. +Jump up ^ Mining Journal 22 January 1853, p. 61 +Benton, Captain James G. (1862). A Course of Instruction in Ordnance and Gunnery (2 ed.). West Point, New York: Thomas Publications. ISBN 1-57747-079-6.. +Brown, G. I. (1998). The Big Bang: A History of Explosives. Sutton Publishing. ISBN 0-7509-1878-0.. +Buchanan, Brenda J., ed. (2006). Gunpowder, Explosives and the State: A Technological History. Aldershot: Ashgate. ISBN 0-7546-5259-9.. +Chase, Kenneth (2003). Firearms: A Global History to 1700. Cambridge University Press. ISBN 0-521-82274-2.. +Cocroft, Wayne (2000). Dangerous Energy: The archaeology of gunpowder and military explosives manufacture. Swindon: English Heritage. ISBN 1-85074-718-0.. +Crosby, Alfred W. (2002). Throwing Fire: Projectile Technology Through History. Cambridge University Press. ISBN 0-521-79158-8.. +Earl, Brian (1978). Cornish Explosives. Cornwall: The Trevithick Society. ISBN 0-904040-13-5.. +al-Hassan, Ahmad Y.. "History of Science and Technology in Islam". |chapter= ignored (help). +Johnson, Norman Gardner. "explosive". Encyclopædia Britannica. Chicago: Encyclopædia Britannica Online.. +Kelly, Jack (2004). Gunpowder: Alchemy, Bombards, & Pyrotechnics: The History of the Explosive that Changed the World. Basic Books. ISBN 0-465-03718-6.. +Khan, Iqtidar Alam (1996). "Coming of Gunpowder to the Islamic World and North India: Spotlight on the Role of the Mongols". Journal of Asian History 30: 41–5.. +Khan, Iqtidar Alam (2004). "Gunpowder and Firearms: Warfare in Medieval India". Oxford University Press. doi:10.1086/ahr.111.3.817.. +Needham, Joseph (1986). "Science & Civilisation in China". V:7: The Gunpowder Epic. Cambridge University Press. ISBN 0-521-30358-3.. +Norris, John (2003). Early Gunpowder Artillery: 1300-1600. Marlborough: The Crowood Press. ISBN 9781861266156.. +Partington, J.R. (1960). A History of Greek Fire and Gunpowder. Cambridge, UK: W. Heffer & Sons.. +Partington, James Riddick; Hall, Bert S. (1999). A History of Greek Fire and Gunpowder. Baltimore: Johns Hopkins University Press. doi:10.1353/tech.2000.0031. ISBN 0-8018-5954-9. +Urbanski, Tadeusz (1967). "Chemistry and Technology of Explosives" III. New York: Pergamon Press.. +External links[edit] + Wikimedia Commons has media related to Gunpowder. + Look up gunpowder in Wiktionary, the free dictionary. +Gun and Gunpowder +The Origins of Gunpowder +Cannons and Gunpowder +Oare Gunpowder Works, Kent, UK +Royal Gunpowder Mills +The DuPont Company on the Brandywine A digital exhibit produced by the Hagley Library that covers the founding and early history of the DuPont Company powder yards in Delaware +"Ulrich Bretschler's Gunpowder Chemistry page". +Video Demonstration of the Medieval Siege Society's Guns, Including showing ignition of gunpowder +Black Powder Recipes +"Dr. Sasse's investigations (and others) found via search at US DTIC.MIL These contain scientific studies of BP properties and details of measurement techniques.". +Categories: GunpowderChinese inventionsExplosivesFirearm propellantsPyrotechnic compositionsRocket fuelsSolid fuels +Navigation menu +Create accountLog inArticleTalkReadEditView history + +Main page +Contents +Featured content +Current events +Random article +Donate to Wikipedia +Wikimedia Shop +Interaction +Help +About Wikipedia +Community portal +Recent changes +Contact page +Tools +What links here +Related changes +Upload file +Special pages +Permanent link +Page information +Wikidata item +Cite this page +Print/export +Create a book +Download as PDF +Printable version +Languages +Afrikaans +العربية +Aragonés +Asturianu +Azərbaycanca +Башҡортса +Беларуская +Беларуская (тарашкевіца)‎ +Български +Bosanski +Brezhoneg +Буряад +Català +Чӑвашла +Čeština +Corsu +Cymraeg +Dansk +Deutsch +Eesti +Ελληνικά +Español +Esperanto +Euskara +فارسی +Français +Gaeilge +Galego +贛語 +Хальмг +한국어 +हिन्दी +Hrvatski +Ilokano +Bahasa Indonesia +Íslenska +Italiano +עברית +Kapampangan +Kiswahili +Kurdî +Latina +Latviešu +Lietuvių +Limburgs +Magyar +Македонски +മലയാളം +مصرى +Монгол +Nederlands +नेपाली +नेपाल भाषा +日本語 +Нохчийн +Norsk bokmål +Norsk nynorsk +Occitan +Oʻzbekcha +پنجابی +Polski +Português +Română +Runa Simi +Русский +Саха тыла +Scots +Shqip +Sicilianu +Simple English +Slovenčina +Slovenščina +کوردی +Српски / srpski +Srpskohrvatski / српскохрватски +Suomi +Svenska +Tagalog +தமிழ் +Татарча/tatarça +ไทย +Türkçe +Українська +اردو +Tiếng Việt +Võro +Winaray +ייִדיש +粵語 +Žemaitėška +中文 +Edit links +This page was last modified on 28 November 2014 at 05:37. +Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc., a non-profit organization. +Privacy policyAbout WikipediaDisclaimersContact WikipediaDevelopersMobile viewWikimedia Foundation Powered by MediaWiki + + +Smokeless powder +From Wikipedia, the free encyclopedia + +Finnish smokeless powder +Smokeless powder is the name given to a number of propellants used in firearms and artillery that produce negligible smoke when fired, unlike the black powder they replaced. The term is unique to the United States and is generally not used in other English-speaking countries, which initially used proprietary names such as "Ballistite" and "Cordite" but gradually shifted to "propellant" as the generic term. +The basis of the term smokeless is that the combustion products are mainly gaseous, compared to around 55% solid products (mostly potassium carbonate, potassium sulfate, and potassium sulfide) for black powder.[1] Despite its name, smokeless powder is not completely smoke-free;[2] while there may be little noticeable smoke from small-arms ammunition, smoke from artillery fire can be substantial. This article focuses on nitrocellulose formulations, but the term smokeless powder was also used to describe various picrate mixtures with nitrate, chlorate, or dichromate oxidizers during the late 19th century, before the advantages of nitrocellulose became evident.[3] +Since the 14th century[4] gunpowder was not actually a physical "powder," and smokeless powder can only be produced as a pelletized or extruded granular material. Smokeless powder allowed the development of modern semi- and fully automatic firearms and lighter breeches and barrels for artillery. Burnt black powder leaves a thick, heavy fouling that is hygroscopic and causes rusting of the barrel. The fouling left by smokeless powder exhibits none of these properties (though some primer compounds can leave hygroscopic salts that have a similar effect; non-corrosive primer compounds were introduced in the 1920s[5][6]). This makes an autoloading firearm with many moving parts feasible (which would otherwise jam or seize under heavy black powder fouling). +Smokeless powders are classified as, typically, division 1.3 explosives under the UN Recommendations on the transportation of Dangerous goods – Model Regulations, regional regulations (such as ADR) and national regulations (such the United States' ATF). However, they are used as solid propellants; in normal use, they undergo deflagration rather than detonation. +Contents [hide] +1 Background +2 Nitroglycerine and guncotton +3 Propellant improvements +4 Chemical formulations +5 Instability and stabilization +6 Physical variations +7 Smokeless propellant components +8 Manufacturing +9 Flashless propellant +10 See also +11 References +11.1 Notes +11.2 Sources +12 External links +Background[edit] +Military commanders had been complaining since the Napoleonic Wars about the problems of giving orders on a battlefield obscured by the smoke of firing. Verbal commands could not be heard above the noise of the guns, and visual signals could not be seen through the thick smoke from the gunpowder used by the guns. Unless there was a strong wind, after a few shots, soldiers using black powder ammunition would have their view obscured by a huge cloud of smoke. Snipers or other concealed shooters were given away by a cloud of smoke over the firing position. Black powder is also corrosive, making cleaning mandatory after every use. Likewise, black powder's tendency to produce severe fouling caused actions to jam and often made reloading difficult. +Nitroglycerine and guncotton[edit] +Nitroglycerine was synthesized by the Italian chemist Ascanio Sobrero in 1847.[7] It was subsequently developed and manufactured by Alfred Nobel as an industrial explosive, but even then it was unsuitable as a propellant: despite its energetic and smokeless qualities, it detonates instead of deflagrating smoothly, making it more amenable to shattering a gun than propelling a projectile out of it. Nitroglycerine per se is also highly unstable, making it unfit to be carried in battlefield conditions. +A major step forward was the discovery of guncotton, a nitrocellulose-based material, by Swiss chemist Christian Friedrich Schönbein in 1846. He promoted its use as a blasting explosive[8] and sold manufacturing rights to the Austrian Empire. Guncotton was more powerful than gunpowder, but at the same time was once again somewhat more unstable. John Taylor obtained an English patent for guncotton; and John Hall & Sons began manufacture in Faversham. +English interest languished after an explosion destroyed the Faversham factory in 1847. Austrian Baron Wilhelm Lenk von Wolfsberg built two guncotton plants producing artillery propellent, but it too was dangerous under field conditions, and guns that could fire thousands of rounds using gunpowder would reach their service life after only a few hundred shots with the more powerful guncotton. Small arms could not withstand the pressures generated by guncotton at all. +After one of the Austrian factories blew up in 1862, Thomas Prentice & Company began manufacturing guncotton in Stowmarket in 1863; and British War Office chemist Sir Frederick Abel began thorough research at Waltham Abbey Royal Gunpowder Mills leading to a manufacturing process that eliminated the impurities in nitrocellulose making it safer to produce and a stable product safer to handle. Abel patented this process in 1865, when the second Austrian guncotton factory exploded. After the Stowmarket factory exploded in 1871, Waltham Abbey began production of guncotton for torpedo and mine warheads.[9] +Propellant improvements[edit] +In 1863, Prussian artillery captain Johann F. E. Schultze patented a small arms propellent of nitrated hardwood impregnated with saltpetre or barium nitrate. Prentice received an 1866 patent for a sporting powder of nitrated paper manufactured at Stowmarket, but ballistic uniformity suffered as the paper absorbed atmospheric moisture. In 1871, Frederick Volkmann received an Austrian patent for a colloided version of Schultze powder called Collodin, which he manufactured near Vienna for use in sporting firearms. Austrian patents were not published at the time, and the Austrian Empire considered the operation a violation of the government monopoly on explosives manufacture and closed the Volkmann factory in 1875.[9] In 1882, the Explosives Company at Stowmarket patented an improved formulation of nitrated cotton gelatinised by ether-alcohol with nitrates of potassium and barium. These propellants were suitable for shotguns but not rifles.[10] + +Poudre B single-base smokeless powder flakes +In 1884, Paul Vieille invented a smokeless powder called Poudre B (short for poudre blanche—white powder, as distinguished from black powder)[11] made from 68.2% insoluble nitrocellulose, 29.8% soluble nitrocellusose gelatinized with ether and 2% paraffin. This was adopted for the Lebel rifle.[12] It was passed through rollers to form paper thin sheets, which were cut into flakes of the desired size.[11] The resulting propellant, today known as pyrocellulose, contains somewhat less nitrogen than guncotton and is less volatile. A particularly good feature of the propellant is that it will not detonate unless it is compressed, making it very safe to handle under normal conditions. +Vieille's powder revolutionized the effectiveness of small guns, because it gave off almost no smoke and was three times more powerful than black powder. Higher muzzle velocity meant a flatter trajectory and less wind drift and bullet drop, making 1000 meter shots practicable. Since less powder was needed to propel a bullet, the cartridge could be made smaller and lighter. This allowed troops to carry more ammunition for the same weight. Also, it would burn even when wet. Black powder ammunition had to be kept dry and was almost always stored and transported in watertight cartridges. +Other European countries swiftly followed and started using their own versions of Poudre B, the first being Germany and Austria, which introduced new weapons in 1888. Subsequently Poudre B was modified several times with various compounds being added and removed. Krupp began adding diphenylamine as a stabilizer in 1888.[9] +Meanwhile, in 1887, Alfred Nobel obtained an English patent for a smokeless gunpowder he called Ballistite. In this propellant the fibrous structure of cotton (nitro-cellulose) was destroyed by a nitro-glycerine solution instead of a solvent.[13] In England in 1889, a similar powder was patented by Hiram Maxim, and in the USA in 1890 by Hudson Maxim.[14] Ballistite was patented in the United States in 1891. +The Germans adopted ballistite for naval use in 1898, calling it WPC/98. The Italians adopted it as filite, in cord instead of flake form, but realising its drawbacks changed to a formulation with nitroglycerine they called solenite. In 1891 the Russians tasked the chemist Mendeleef with finding a suitable propellant, he created nitrocellulose gelatinised by ether-alcohol, which produced more nitrogen and more uniform colloidal structure than the French use of nitro-cottons in Poudre B. He called it pyro-collodion.[13] +Britain conducted trials on all the various types of propellant brought to their attention, but were dissatisfied with them all and sought something superior to all existing types. In 1889, Sir Frederick Abel, James Dewar and Dr W Kellner patented (Nos 5614 and 11,664 in the names of Abel and Dewar) a new formulation that was manufactured at the Royal Gunpowder Factory at Waltham Abbey. It entered British service in 1891 as Cordite Mark 1. Its main composition was 58% Nitro-glycerine, 37% Guncotton and 3% mineral jelly. A modified version, Cordite MD, entered service in 1901, this increased guncotton to 65% and reduced nitro-glycerine to 30%, this change reduced the combustion temperature and hence erosion and barrel wear. Cordite's advantages over gunpowder were reduced maximum pressure in the chamber (hence lighter breeches, etc.) but longer high pressure. Cordite could be made in any desired shape or size.[15] The creation of cordite led to a lengthy court battle between Nobel, Maxim, and another inventor over alleged British patent infringement. +The Anglo-American Explosives Company began manufacturing its shotgun powder in Oakland, New Jersey in 1890. DuPont began producing guncotton at Carneys Point Township, New Jersey in 1891.[3] Charles E. Munroe of the Naval Torpedo Station in Newport, Rhode Island patented a formulation of guncotton colloided with nitrobenzene, called Indurite, in 1891.[16] Several United States firms began producing smokeless powder when Winchester Repeating Arms Company started loading sporting cartridges with Explosives Company powder in 1893. California Powder Works began producing a mixture of nitroglycerine and nitrocellulose with ammonium picrate as Peyton Powder, Leonard Smokeless Powder Company began producing nitroglycerine-nitrocellulose Ruby powders, Laflin & Rand negotiated a license to produce Ballistite, and DuPont started producing smokeless shotgun powder. The United States Army evaluated 25 varieties of smokeless powder and selected Ruby and Peyton Powders as the most suitable for use in the Krag-Jørgensen service rifle. Ruby was preferred, because tin-plating was required to protect brass cartridge cases from picric acid in the Peyton Powder. Rather than paying the required royalties for Ballistite, Laflin & Rand financed Leonard's reorganization as the American Smokeless Powder Company. United States Army Lieutenant Whistler assisted American Smokeless Powder Company factory superintendent Aspinwall in formulating an improved powder named W.A. for their efforts. W.A. smokeless powder was the standard for United States military service rifles from 1897 until 1908.[3] +In 1897, United States Navy Lieutenant John Bernadou patented a nitrocellulose powder colloided with ether-alcohol.[16] The Navy licensed or sold patents for this formulation to DuPont and the California Powder Works while retaining manufacturing rights for the Naval Powder Factory, Indian Head, Maryland constructed in 1900. The United States Army adopted the Navy single-base formulation in 1908 and began manufacture at Picatinny Arsenal.[3] By that time Laflin & Rand had taken over the American Powder Company to protect their investment, and Laflin & Rand had been purchased by DuPont in 1902.[17] Upon securing a 99-year lease of the Explosives Company in 1903, DuPont enjoyed use of all significant smokeless powder patents in the United States, and was able to optimize production of smokeless powder.[3] When government anti-trust action forced divestiture in 1912, DuPont retained the nitrocellulose smokeless powder formulations used by the United States military and released the double-base formulations used in sporting ammunition to the reorganized Hercules Powder Company. These newer propellants were more stable and thus safer to handle than Poudre B, and also more powerful. +Chemical formulations[edit] +"Double base" redirects here. For the musical instrument, see double bass. +Currently, propellants using nitrocellulose (detonation velocity 7,300 m/s (23,950 ft/s)) (typically an ether-alcohol colloid of nitrocellulose) as the sole explosive propellant ingredient are described as single-base powder.[18] +Propellants mixtures containing nitrocellulose and nitroglycerin (detonation velocity 7,700 m/s (25,260 ft/s)) as explosive propellant ingredients are known as double-base powder.[19] +During the 1930s triple-base propellant containing nitrocellulose, nitroglycerin, and a substantial quantity of nitroguanidine (detonation velocity 8,200 m/s (26,900 ft/s)) as explosive propellant ingredients was developed. These propellant mixtures have reduced flash and flame temperature without sacrificing chamber pressure compared to single and double base propellants, albeit at the cost of more smoke. +In practice, triple base propellants are reserved mainly for large caliber ammunition such as used in (naval) artillery and tank guns. During World War II it had some use by British artillery. After that war it became the standard propellant in all British large caliber ammunition designs except small-arms. Most western nations, except the United States, followed a similar path. +In the late 20th century new propellant formulations started to appear. These are based on nitroguanidine and high explosives of the RDX (detonation velocity 8,750 m/s (28,710 ft/s)) type. +Instability and stabilization[edit] +Nitrocellulose deteriorates with time, yielding acidic byproducts. Those byproducts catalyze the further deterioration, increasing its rate. The released heat, in case of bulk storage of the powder, or too large blocks of solid propellant, can cause self-ignition of the material. Single-base nitrocellulose propellants are hygroscopic and most susceptible to degradation; double-base and triple-base propellants tend to deteriorate more slowly. To neutralize the decomposition products, which could otherwise cause corrosion of metals of the cartridges and gun barrels, calcium carbonate is added to some formulations. +To prevent buildup of the deterioration products, stabilizers are added. Diphenylamine is one of the most common stabilizers used. Nitrated analogs of diphenylamine formed in the process of stabilizing decomposing powder are sometimes used as stabilizers themselves.[20][21] The stabilizers are added in the amount of 0.5–2% of the total amount of the formulation; higher amounts tend to degrade its ballistic properties. The amount of the stabilizer is depleted with time. Propellants in storage should be periodically tested for the amount of stabilizer remaining, as its depletion may lead to auto-ignition of the propellant. +Physical variations[edit] + +Ammunition handloading powders +Smokeless powder may be corned into small spherical balls or extruded into cylinders or strips with many cross-sectional shapes (strips with various rectangular proportions, single or multi-hole cylinders, slotted cylinders) using solvents such as ether. These extrusions can be cut into short ('flakes') or long pieces ('cords' many inches long). Cannon powder has the largest pieces. +The properties of the propellant are greatly influenced by the size and shape of its pieces. The specific surface area of the propellant influences the speed of burning, and the size and shape of the particles determine the specific surface area. By manipulation of the shape it is possible to influence the burning rate and hence the rate at which pressure builds during combustion. Smokeless powder burns only on the surfaces of the pieces. Larger pieces burn more slowly, and the burn rate is further controlled by flame-deterrent coatings that retard burning slightly. The intent is to regulate the burn rate so that a more or less constant pressure is exerted on the propelled projectile as long as it is in the barrel so as to obtain the highest velocity. The perforations stabilize the burn rate because as the outside burns inward (thus shrinking the burning surface area) the inside is burning outward (thus increasing the burning surface area, but faster, so as to fill up the increasing volume of barrel presented by the departing projectile).[22] Fast-burning pistol powders are made by extruding shapes with more area such as flakes or by flattening the spherical granules. Drying is usually performed under a vacuum. The solvents are condensed and recycled. The granules are also coated with graphite to prevent static electricity sparks from causing undesired ignitions.[23] +Faster-burning propellants generate higher temperatures and higher pressures, however they also increase wear on gun barrels. +Smokeless propellant components[edit] +The propellant formulations may contain various energetic and auxiliary components: +Propellants: +Nitrocellulose, an energetic component of most smokeless propellants[24] +Nitroglycerin, an energetic component of double-base and triple-base formulations[24] +Nitroguanidine, a component of triple-base formulations[24] +D1NA (bis-nitroxyethylnitramine)[25] +Fivonite (tetramethylolcyclopentanone)[25] +DGN (di-ethylene glycol dinitrate)[26] +Acetyl cellulose[27] +Deterrents, (or moderants), to slow the burning rate +Centralites (symmetrical diphenyl urea—primarily diethyl or dimethyl)[28][29] +Dibutyl phthalate[24][29] +Dinitrotoluene (toxic, carcinogenic, and obsolete)[24][30] +Akardite (asymmetrical diphenyl urea)[26] +ortho-tolyl urethane[31] +Polyester adipate +Camphor (obsolete)[29] +Stabilizers, to prevent or slow down self-decomposition[32] +Diphenylamine[33] +Petroleum jelly[34] +Calcium carbonate[24] +Magnesium oxide[26] +Sodium bicarbonate[27] +beta-naphthol methyl ether[31] +Amyl alcohol (obsolete)[35] +Aniline (obsolete)[36] +Decoppering additives, to hinder the buildup of copper residues from the gun barrel rifling +Tin metal and compounds (e.g., tin dioxide)[24][37] +Bismuth metal and compounds (e.g., bismuth trioxide, bismuth subcarbonate, bismuth nitrate, bismuth antimonide); the bismuth compounds are favored as copper dissolves in molten bismuth, forming brittle and easily removable alloy +Lead foil and lead compounds, phased out due to toxicity[25] +Flash reducers, to reduce the brightness of the muzzle flash (all have a disadvantage: the production of smoke)[38] +Potassium chloride[39] +Potassium nitrate +Potassium sulfate[24][37] +Potassium hydrogen tartarate (a byproduct of wine production formerly used by French artillery)[39] +Wear reduction additives, to lower the wear of the gun barrel liners[40] +Wax +Talc +Titanium dioxide +Polyurethane jackets over the powder bags, in large guns +Other additives +Ethyl acetate, a solvent for manufacture of spherical powder[34] +Rosin, a surfactant to hold the grain shape of spherical powder +Graphite, a lubricant to cover the grains and prevent them from sticking together, and to dissipate static electricity[23] +Manufacturing[edit] +This section describes procedures used in the United States. See Cordite for alternative procedures formerly used in the United Kingdom. +The United States Navy manufactured single-base tubular powder for naval artillery at Indian Head, Maryland, beginning in 1900. Similar procedures were used for United States Army production at Picatinny Arsenal beginning in 1907[18] and for manufacture of smaller grained Improved Military Rifle (IMR) powders after 1914. Short-fiber cotton linter was boiled in a solution of sodium hydroxide to remove vegetable waxes, and then dried before conversion to nitrocellulose by mixing with concentrated nitric and sulfuric acids. Nitrocellulose still resembles fibrous cotton at this point in the manufacturing process, and was typically identified as pyrocellulose because it would spontaneously ignite in air until unreacted acid was removed. The term guncotton was also used; although some references identify guncotton as a more extensively nitrated and refined product used in torpedo and mine warheads prior to use of TNT.[41] +Unreacted acid was removed from pyrocellulose pulp by a multistage draining and water washing process similar to that used in paper mills during production of chemical woodpulp. Pressurized alcohol removed remaining water from drained pyrocellulose prior to mixing with ether and diphenylamine. The mixture was then fed through a press extruding a long turbular cord form to be cut into grains of the desired length.[42] +Alcohol and ether were then evaporated from "green" powder grains to a remaining solvent concentration between 3 percent for rifle powders and 7 percent for large artillery powder grains. Burning rate is inversely proportional to solvent concentration. Grains were coated with electrically conductive graphite to minimize generation of static electricity during subsequent blending. "Lots" containing more than ten tonnes of powder grains were mixed through a tower arrangement of blending hoppers to minimize ballistic differences. Each blended lot was then subjected to testing to determine the correct loading charge for the desired performance.[43][44] +Military quantities of old smokeless powder were sometimes reworked into new lots of propellants.[45] Through the 1920s Dr. Fred Olsen worked at Picatinny Arsenal experimenting with ways to salvage tons of single-base cannon powder manufactured for World War I. Dr. Olsen was employed by Western Cartridge Company in 1929 and developed a process for manufacturing spherical smokeless powder by 1933.[46] Reworked powder or washed pyrocellulose can be dissolved in ethyl acetate containing small quantities of desired stabilizers and other additives. The resultant syrup, combined with water and surfactants, can be heated and agitated in a pressurized container until the syrup forms an emulsion of small spherical globules of the desired size. Ethyl acetate distills off as pressure is slowly reduced to leave small spheres of nitrocellulose and additives. The spheres can be subsequently modified by adding nitroglycerine to increase energy, flattening between rollers to a uniform minimum dimension, coating with phthalate deterrents to retard ignition, and/or glazing with graphite to improve flow characteristics during blending.[47][48] +Modern smokeless powder is produced in the United States by St. Marks Powder, Inc. owned by General Dynamics.[49] +Flashless propellant[edit] +Muzzle flash is the light emitted in the vicinity of the muzzle by the hot propellant gases and the chemical reactions that follow as the gases mix with the surrounding air. Before projectiles exit a slight pre-flash may occur from gases leaking past the projectiles. Following muzzle exit the heat of gases is usually sufficient to emit visible radiation – the primary flash. The gases expand but as they pass through the Mach disc they are re-compressed to produce an intermediate flash. Hot combustible gases (e.g. hydrogen and carbon-monoxide) may follow when they mix with oxygen in the surrounding air to produce the secondary flash, the brightest. The secondary flash does not usually occur with small-arms.[50] +Nitrocellulose contains insufficient oxygen to completely oxidize its carbon and hydrogen. The oxygen deficit is increased by addition of graphite and organic stabilizers. Products of combustion within the gun barrel include flammable gasses like hydrogen and carbon monoxide. At high temperature, these flammable gasses will ignite when turbulently mixed with atmospheric oxygen beyond the muzzle of the gun. During night engagements the flash produced by ignition can reveal the location of the gun to enemy forces[51] and cause temporary night-blindness among the gun crew by photo-bleaching visual purple.[52] +Flash suppressors are commonly used on small arms to reduce the flash signature, but this approach is not practical for artillery. Artillery muzzle flash up to 150 feet (46 m) from the muzzle has been observed, and can be reflected off clouds and be visible for distances up to 30 miles (48 km).[51] For artillery the most effective method is a propellant that produces a large proportion of inert nitrogen at relatively low temperatures that dilutes the combustible gases. Triple based propellants are used for this because of the nitrogen in the nitroguandine.[53] +Before the use of triple based propellants the usual method of flash reduction was to add inorganic salts like potassium chloride so their specific heat capacity might reduce the temperature of combustion gasses and their finely divided particulate smoke might block visible wavelengths of radiant energy of combustion.[39] +See also[edit] +Portal icon Pyrotechnics portal +Antique guns +Ballistite +Cordite +Firearms +Gunpowder +Nitrocellulose +Small arms +Brown-brown – a drug created by mixing cocaine with cartridge powder +References[edit] +Notes[edit] +Jump up ^ Hatcher, Julian S. and Barr, Al Handloading Hennage Lithograph Company (1951) p.34 +Jump up ^ Fairfield, A. P., CDR USN Naval Ordnance Lord Baltimore Press (1921) p.44 +^ Jump up to: a b c d e Sharpe, Philip B. Complete Guide to Handloading 3rd Edition (1953) Funk & Wagnalls pp.146-149 +Jump up ^ seegunpowder +Jump up ^ Sharpe, Philip B. Complete Guide To Handloading (1953) Funk & Wagnalls p.60 +Jump up ^ Davis, William C., Jr. Handloading (1981) National Rifle Association p.21 +Jump up ^ Davis, Tenney L. The Chemistry of Powder & Explosives (1943) page 195 +Jump up ^ Davis, William C., Jr. Handloading National Rifle Association of America (1981) p.28 +^ Jump up to: a b c Sharpe, Philip B. Complete Guide to Handloading 3rd Edition (1953) Funk & Wagnalls pp.141-144 +Jump up ^ Hogg, Oliver F. G. Artillery: Its Origin, Heyday and Decline (1969) p.138-139 +^ Jump up to: a b Davis, Tenney L. The Chemistry of Powder & Explosives (1943) pages 289–292 +Jump up ^ Hogg, Oliver F. G. Artillery: Its Origin, Heyday and Decline (1969) p.139 +^ Jump up to: a b Hogg, Oliver F. G. Artillery: Its Origin, Heyday and Decline (1969) p.140 +Jump up ^ U.S. Patent 430,212 – Manufacture of explosive – H. S. Maxim +Jump up ^ Hogg, Oliver F. G. Artillery: Its Origin, Heyday and Decline (1969) p.141 +^ Jump up to: a b Davis, Tenney L. The Chemistry of Powder & Explosives (1943) pages 296-297 +Jump up ^ "Laflin & Rand Powder Company". DuPont. Retrieved 2012-02-24. +^ Jump up to: a b Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p.297 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p.298 +Jump up ^ Fairfield, A. P., CDR USN Naval Ordnance Lord Baltimore Press (1921) p.28 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p. 310 +Jump up ^ Fairfield, A. P., CDR USN Naval Ordnance Lord Baltimore Press (1921) pp.41–43 +^ Jump up to: a b Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p.306 +^ Jump up to: a b c d e f g h Campbell, John Naval Weapons of World War Two (1985) p. 5 +^ Jump up to: a b c Campbell, John Naval Weapons of World War Two (1985) p. 104 +^ Jump up to: a b c Campbell, John Naval Weapons of World War Two (1985) p. 221 +^ Jump up to: a b Campbell, John Naval Weapons of World War Two (1985) p. 318 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) pages 317–320 +^ Jump up to: a b c Davis, William C., Jr. Handloading National Rifle Association of America (1981) p.30 +Jump up ^ Davis, William C., Jr. Handloading National Rifle Association of America (1981) p.31 +^ Jump up to: a b Campbell, John Naval Weapons of World War Two (1985) p. 174 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) pages 307–311 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p. 302 +^ Jump up to: a b Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p. 296 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p. 307 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) p. 308 +^ Jump up to: a b Davis, William C., Jr. Handloading National Rifle Association of America (1981) p.32 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) pages 322–327 +^ Jump up to: a b c Davis, Tenny L. The Chemistry of Powder & Explosives (1943) pages 323–327 +Jump up ^ "USA 16"/50 (40.6 cm) Mark 7". NavWeaps. 2008-11-03. Retrieved 2008-12-05. +Jump up ^ Fairfield, A. P., CDR USN Naval Ordnance Lord Baltimore Press (1921) pages 28–31 +Jump up ^ Fairfield, A. P., CDR USN Naval Ordnance Lord Baltimore Press (1921) pages 31–35 +Jump up ^ Fairfield, A. P., CDR USN Naval Ordnance Lord Baltimore Press (1921) pages 35–41 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) pages 293 & 306 +Jump up ^ Fairfield, A. P., CDR USN Naval Ordnance Lord Baltimore Press (1921) p.39 +Jump up ^ Matunas, E. A. Winchester-Western Ball Powder Loading Data Olin Corporation (1978) p.3 +Jump up ^ Davis, Tenny L. The Chemistry of Powder & Explosives (1943) pages 328–330 +Jump up ^ Wolfe, Dave Propellant Profiles Volume 1 Wolfe Publishing Company (1982) pages 136–137 +Jump up ^ General Dynamics Commercial Powder Applications. +Jump up ^ Moss G. M., Leeming D. W., Farrar C. L. Military Ballisitcs (1969) pages 55–56 +^ Jump up to: a b Davis, Tenny L. The Chemistry of Powder & Explosives (1943) pages 322–323 +Jump up ^ Milner p.68 +Jump up ^ Moss G. M., Leeming D. W., Farrar C. L. Military Ballisitcs (1969) pages 59–60 +Sources[edit] +Campbell, John (1985). Naval Weapons of World War Two. Naval Institute Press. ISBN 0-87021-459-4. +Davis, Tenney L. (1943). The Chemistry of Powder & Explosives (Angriff Press [1992] ed.). John Wiley & Sons Inc. ISBN 0-913022-00-4. +Davis, William C., Jr. (1981). Handloading. National Rifle Association of America. ISBN 0-935998-34-9. +Fairfield, A. P., CDR USN (1921). Naval Ordnance. Lord Baltimore Press. +Hatcher, Julian S. and Barr, Al (1951). Handloading. Hennage Lithograph Company. +Matunas, E. A. (1978). Winchester-Western Ball Powder Loading Data. Olin Corporation. +Milner, Marc (1985). North Atlantic Run. Naval Institute Press. ISBN 0-87021-450-0. +Wolfe, Dave (1982). Propellant Profiles Volume 1. Wolfe Publishing Company. ISBN 0-935632-10-7. +External links[edit] +The Manufacture of Smokeless Powders and their Forensic Analysis: A Brief Review – Robert M. Heramb, Bruce R. McCord +Hudson Maxim papers (1851-1925) at Hagley Museum and Library. Collection includes material relating to Maxim's patent on the process of making smokeless powder. +Categories: CorditeExplosivesFirearm propellantsSolid fuels +Navigation menu +Create accountLog inArticleTalkReadEditView history + +Main page +Contents +Featured content +Current events +Random article +Donate to Wikipedia +Wikimedia Shop +Interaction +Help +About Wikipedia +Community portal +Recent changes +Contact page +Tools +What links here +Related changes +Upload file +Special pages +Permanent link +Page information +Wikidata item +Cite this page +Print/export +Create a book +Download as PDF +Printable version +Languages +العربية +Български +Dansk +Deutsch +Español +فارسی +Français +Bahasa Indonesia +Íslenska +Italiano +עברית +Nederlands +日本語 +Polski +Português +Русский +Svenska +தமிழ் +中文 +Edit links +This page was last modified on 25 July 2014 at 22:33. +Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc., a non-profit organization. +Privacy policyAbout WikipediaDisclaimersContact WikipediaDevelopersMobile viewWikimedia Foundation Powered by MediaWiki + + +Deflagration +From Wikipedia, the free encyclopedia + +[hide]This article has multiple issues. Please help improve it or discuss these issues on the talk page. +This article needs additional citations for verification. (April 2011) +This article may be too technical for most readers to understand. (December 2013) + +A log in a fireplace. +Deflagration [1] (Lat: de + flagrare, "to burn down") is a term describing subsonic combustion propagating through heat transfer; hot burning material heats the next layer of cold material and ignites it. Most "fire" found in daily life, from flames to explosions, is deflagration. Deflagration is different from detonation, which is supersonic and propagates through shock. +Contents [hide] +1 Applications +2 Oil/wax fire and water +3 Flame physics +4 Damaging deflagration events +5 See also +6 References +Applications[edit] +In engineering applications, deflagrations are easier to control than detonations. Consequently, they are better suited when the goal is to move an object (a bullet in a gun, or a piston in an internal combustion engine) with the force of the expanding gas. Typical examples of deflagrations are the combustion of a gas-air mixture in a gas stove or a fuel-air mixture in an internal combustion engine, and the rapid burning of gunpowder in a firearm or of pyrotechnic mixtures in fireworks. Deflagration systems and products can also be used in mining, demolition and stone quarrying via gas pressure blasting as a beneficial alternative to high explosives. +Oil/wax fire and water[edit] +Adding water to a burning hydrocarbon such as oil or wax produces a deflagration. The water boils rapidly and ejects the burning material as a fine spray of droplets. A deflagration then occurs as the fine mist of oil ignites and burns extremely rapidly. These are particularly common in chip pan fires, which are responsible for one in five household fires in Britain.[2] +Flame physics[edit] +The underlying flame physics can be understood with the help of an idealized model consisting of a uniform one-dimensional tube of unburnt and burned gaseous fuel, separated by a thin transitional region of width \delta\; in which the burning occurs. The burning region is commonly referred to as the flame or flame front. In equilibrium, thermal diffusion across the flame front is balanced by the heat supplied by burning. +There are two characteristic timescales which are important here. The first is the thermal diffusion timescale \tau_d\;, which is approximately equal to +\tau_d \simeq \delta^2 / \kappa, +where \kappa \; is the thermal diffusivity. The second is the burning timescale \tau_b that strongly decreases with temperature, typically as +\tau_b\propto \exp[\Delta U/(k_B T_f)], +where \Delta U\; is the activation barrier for the burning reaction and T_f\; is the temperature developed as the result of burning; the value of this so-called "flame temperature" can be determined from the laws of thermodynamics. +For a stationary moving deflagration front, these two timescales must be equal: the heat generated by burning is equal to the heat carried away by heat transfer. This makes it possible to calculate the characteristic width \delta\; of the flame front: +\tau_b = \tau_d\;, +thus + \delta \simeq \sqrt {\kappa \tau_b} . +Now, the thermal flame front propagates at a characteristic speed S_l\;, which is simply equal to the flame width divided by the burn time: +S_l \simeq \delta / \tau_b \simeq \sqrt {\kappa / \tau_b} . +This simplified model neglects the change of temperature and thus the burning rate across the deflagration front. This model also neglects the possible influence of turbulence. As a result, this derivation gives only the laminar flame speed -- hence the designation S_l\;. +Damaging deflagration events[edit] +Damage to buildings, equipment and people can result from a large-scale, short-duration deflagration. The potential damage is primarily a function of the total amount of fuel burned in the event (total energy available), the maximum flame velocity that is achieved, and the manner in which the expansion of the combustion gases is contained. +In free-air deflagrations, there is a continuous variation in deflagration effects relative to the maximum flame velocity. When flame velocities are low, the effect of a deflagration is to release heat. Some authors use the term flash fire to describe these low-speed deflagrations. At flame velocities near the speed of sound, the energy released is in the form of pressure and the results resemble a detonation. Between these extremes both heat and pressure are released. +When a low-speed deflagration occurs within a closed vessel or structure, pressure effects can produce damage due to expansion of gases as a secondary effect. The heat released by the deflagration causes the combustion gases and excess air to expand thermally. The net result is that the volume of the vessel or structure must expand to accommodate the hot combustion gases, or the vessel must be strong enough to withstand the additional internal pressure, or it fails, allowing the gases to escape. The risks of deflagration inside waste storage drums is a growing concern in storage facilities. +See also[edit] + Look up deflagration in Wiktionary, the free dictionary. +Pressure piling +References[edit] +Jump up ^ "Glossary D-H". Hutchisonrodway.co.nz. Retrieved 2013-12-29. +Jump up ^ UK Fire Service advice on chip pan fires +Categories: Explosives +Navigation menu +Create accountLog inArticleTalkReadEditView history + +Main page +Contents +Featured content +Current events +Random article +Donate to Wikipedia +Wikimedia Shop +Interaction +Help +About Wikipedia +Community portal +Recent changes +Contact page +Tools +What links here +Related changes +Upload file +Special pages +Permanent link +Page information +Wikidata item +Cite this page +Print/export +Create a book +Download as PDF +Printable version +Languages +Català +Čeština +Deutsch +Español +Français +Italiano +Lietuvių +Nederlands +Norsk bokmål +Polski +Português +Русский +Српски / srpski +Svenska +Edit links +This page was last modified on 2 October 2014 at 16:44. +Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc., a non-profit organization. +Privacy policyAbout WikipediaDisclaimersContact WikipediaDevelopersMobile viewWikimedia Foundation Powered by MediaWiki + + +United Kingdom +From Wikipedia, the free encyclopedia +This article is about the sovereign state. For the island, see Great Britain. For other uses, see United Kingdom (disambiguation) and UK (disambiguation). +Page semi-protected +United Kingdom of Great +Britain and Northern Ireland[show] + +A flag featuring both cross and saltire in red, white and blue Coat of arms containing shield and crown in centre, flanked by lion and unicorn +Flag Royal coat of arms[nb 1] +Anthem: "God Save the Queen"[nb 2] +MENU0:00 +Two islands to the north-west of continental Europe. Highlighted are the larger island and the north-eastern fifth of the smaller island to the west. +Location of the United Kingdom (dark green) +– in Europe (green & dark grey) +– in the European Union (green) +Capital +and largest city London +51°30′N 0°7′W +Official language +and national language English +Recognised regional +languages Cornish, Irish, Scots, Scottish Gaelic, Ulster-Scots, Welsh[nb 3] +Ethnic groups (2011) 87.1% White +7.0% Asian +3.0% Black +2.0% Mixed +0.9% Other +Demonym British, Briton +Government Unitary parliamentary constitutional monarchy + - Monarch Elizabeth II + - Prime Minister David Cameron +Legislature Parliament + - Upper house House of Lords + - Lower house House of Commons +Formation + - Acts of Union 1707 1 May 1707 + - Acts of Union 1800 1 January 1801 + - Irish Free State Constitution Act 5 December 1922 +Area + - Total 243,610 km2 (80th) +94,060 sq mi + - Water (%) 1.34 +Population + - 2013 estimate 64,100,000[3] (22nd) + - 2011 census 63,181,775[4] (22nd) + - Density 255.6/km2 (51st) +661.9/sq mi +GDP (PPP) 2014 estimate + - Total $2.435 trillion[5] (10th) + - Per capita $37,744[5] (27th) +GDP (nominal) 2014 estimate + - Total $2.848 trillion[5] (6th) + - Per capita $44,141[5] (22nd) +Gini (2012) positive decrease 32.8[6] +medium · 33rd +HDI (2013) Steady 0.892[7] +very high · 14th +Currency Pound sterling (GBP) +Time zone GMT (UTC​) + - Summer (DST) BST (UTC+1) +Date format dd/mm/yyyy (AD) +Drives on the left +Calling code +44 +ISO 3166 code GB +Internet TLD .uk +The United Kingdom of Great Britain and Northern Ireland Listeni/ɡreɪt ˈbrɪt(ə)n ənd ˈnɔːð(ə)n ˈʌɪələnd/, commonly known as the United Kingdom (UK) or Britain, is a sovereign state in Europe. Lying off the north-western coast of the European mainland, the country includes the island of Great Britain (a term also applied loosely to refer to the whole country),[8] the north-eastern part of the island of Ireland, and many smaller islands. Northern Ireland is the only part of the UK that shares a land border with another state: the Republic of Ireland.[nb 4] Apart from this land border, the UK is surrounded by the Atlantic Ocean, with the North Sea in the east and the English Channel in the south. The Irish Sea lies between Great Britain and Ireland. The UK has an area of 243,610 square kilometres (94,060 sq mi), making it the 78th-largest sovereign state in the world and the 11th-largest in Europe. +The United Kingdom is the 22nd-most populous country, with an estimated 64.1 million inhabitants.[3] It is a constitutional monarchy with a parliamentary system of governance.[9][10] Its capital city is London, an important global city and financial centre with the fourth-largest urban area in Europe.[11] The current monarch—since 6 February 1952—is Queen Elizabeth II. The UK consists of four countries: England, Scotland, Wales, and Northern Ireland.[12] The latter three have devolved administrations,[13] each with varying powers,[14][15] based in their capitals, Edinburgh, Cardiff, and Belfast, respectively. Guernsey, Jersey, and the Isle of Man are not part of the United Kingdom, being Crown dependencies with the British Government responsible for defence and international representation.[16] The UK has fourteen Overseas Territories,[17] including the disputed Falkland Islands, Gibraltar, and Indian Ocean Territory. +The relationships among the countries of the United Kingdom have changed over time. Wales was annexed by the Kingdom of England under the Acts of Union of 1536 and 1543. A treaty between England and Scotland resulted in a unified Kingdom of Great Britain in 1707, which in 1801, merged with the Kingdom of Ireland to form the United Kingdom of Great Britain and Ireland. In 1922, five-sixths of Ireland seceded from the country, leaving the present formulation of the United Kingdom of Great Britain and Northern Ireland.[nb 5] British Overseas Territories, formerly colonies, are the remnants of the British Empire which, at its height in the late 19th and early 20th centuries, encompassed almost a quarter of the world's land mass and was the largest empire in history. British influence can be observed in the language, culture, and legal systems of many of its former colonies. +The United Kingdom is a developed country and has the world's sixth-largest economy by nominal GDP and tenth-largest by purchasing power parity. The country is considered to have a high-income economy and is categorised as very high in the Human Development Index, currently ranking 14th in the world. It was the world's first industrialised country and the world's foremost power during the 19th and early 20th centuries.[18][19] The UK remains a great power with considerable economic, cultural, military, scientific, and political influence internationally.[20][21] It is a recognised nuclear weapons state and its military expenditure ranks fifth or sixth in the world.[22][23] The UK has been a permanent member of the United Nations Security Council since its first session in 1946. It has been a member state of the European Union (EU) and its predecessor, the European Economic Community (EEC), since 1973; it is also a member of the Commonwealth of Nations, the Council of Europe, the G7, the G8, the G20, NATO, the Organisation for Economic Co-operation and Development (OECD), and the World Trade Organization (WTO). +Contents [hide] +1 Etymology and terminology +2 History +2.1 Before 1707 +2.2 Since the Acts of Union of 1707 +3 Geography +3.1 Climate +3.2 Administrative divisions +4 Dependencies +5 Politics +5.1 Government +5.2 Devolved administrations +5.3 Law and criminal justice +5.4 Foreign relations +5.5 Military +6 Economy +6.1 Science and technology +6.2 Transport +6.3 Energy +7 Demographics +7.1 Ethnic groups +7.2 Languages +7.3 Religion +7.4 Migration +7.5 Education +7.6 Healthcare +8 Culture +8.1 Literature +8.2 Music +8.3 Visual art +8.4 Cinema +8.5 Media +8.6 Philosophy +8.7 Sport +8.8 Symbols +9 See also +10 Notes +11 References +12 Further reading +13 External links +Etymology and terminology +See also: Britain (placename) and Terminology of the British Isles +The 1707 Acts of Union declared that the kingdoms of England and Scotland were "United into One Kingdom by the Name of Great Britain", though the new state is also referred to in the Acts as the "Kingdom of Great Britain", "United Kingdom of Great Britain" and "United Kingdom".[24][25][nb 6] However, the term "united kingdom" is only found in informal use during the 18th century and the country was only occasionally referred to as he "United Kingdom of Great Britain".[26] The Acts of Union 1800 united the Kingdom of Great Britain and the Kingdom of Ireland in 1801, forming the United Kingdom of Great Britain and Ireland. The name "United Kingdom of Great Britain and Northern Ireland" was adopted following the independence of the Irish Free State, and the partition of Ireland, in 1922, which left Northern Ireland as the only part of the island of Ireland within the UK.[27] +Although the United Kingdom, as a sovereign state, is a country, England, Scotland, Wales, and to a lesser degree, Northern Ireland, are also regarded as countries, though they are not sovereign states.[28][29] Scotland, Wales and Northern Ireland have devolved self-government.[30][31] The British Prime Minister's website has used the phrase "countries within a country" to describe the United Kingdom.[12] Some statistical summaries, such as those for the twelve NUTS 1 regions of the UK, also refer to Scotland, Wales and Northern Ireland as "regions".[32][33] Northern Ireland is also referred to as a "province".[28][34] With regard to Northern Ireland, the descriptive name used "can be controversial, with the choice often revealing one's political preferences."[35] +The term Britain is often used as synonym for the United Kingdom. The term Great Britain, by contrast, refers conventionally to the island of Great Britain, or politically to England, Scotland and Wales in combination.[36][37][38] However, it is sometimes used as a loose synonym for the United Kingdom as a whole.[39][40] GB and GBR are the standard country codes for the United Kingdom (see ISO 3166-2 and ISO 3166-1 alpha-3) and are consequently used by international organisations to refer to the United Kingdom. Additionally, the United Kingdom's Olympic team competes under the name "Great Britain" or "Team GB".[41][42] +The adjective British is commonly used to refer to matters relating to the United Kingdom. The term has no definite legal connotation, but is used in law to refer to UK citizenship and matters to do with nationality.[43] People of the United Kingdom use a number of different terms to describe their national identity and may identify themselves as being British; or as being English, Scottish, Welsh, Northern Irish, or Irish;[44] or as being both.[45] +In 2006, a new design of British passport was introduced. Its first page shows the long form name of the state in English, Welsh and Scottish Gaelic.[46] In Welsh, the long form name of the state is "Teyrnas Unedig Prydain Fawr a Gogledd Iwerddon" with "Teyrnas Unedig" being used as a short form name on government websites.[47] In Scottish Gaelic, the long form is "Rìoghachd Aonaichte Bhreatainn is Èireann a Tuath" and the short form "Rìoghachd Aonaichte". +History +See also: History of the British Isles +Before 1707 + +Stonehenge, in Wiltshire, was erected around 2500 BC. +Main articles: History of England, History of Wales, History of Scotland, History of Ireland and History of the formation of the United Kingdom +Settlement by anatomically modern humans of what was to become the United Kingdom occurred in waves beginning by about 30,000 years ago.[48] By the end of the region's prehistoric period, the population is thought to have belonged, in the main, to a culture termed Insular Celtic, comprising Brythonic Britain and Gaelic Ireland.[49] The Roman conquest, beginning in 43 AD, and the 400-year rule of southern Britain, was followed by an invasion by Germanic Anglo-Saxon settlers, reducing the Brythonic area mainly to what was to become Wales and the historic Kingdom of Strathclyde.[50] Most of the region settled by the Anglo-Saxons became unified as the Kingdom of England in the 10th century.[51] Meanwhile, Gaelic-speakers in north west Britain (with connections to the north-east of Ireland and traditionally supposed to have migrated from there in the 5th century)[52][53] united with the Picts to create the Kingdom of Scotland in the 9th century.[54] +In 1066, the Normans invaded England from France and after its conquest, seized large parts of Wales, conquered much of Ireland and were invited to settle in Scotland, bringing to each country feudalism on the Northern French model and Norman-French culture.[55] The Norman elites greatly influenced, but eventually assimilated with, each of the local cultures.[56] Subsequent medieval English kings completed the conquest of Wales and made an unsuccessful attempt to annex Scotland. Thereafter, Scotland maintained its independence, albeit in near-constant conflict with England. The English monarchs, through inheritance of substantial territories in France and claims to the French crown, were also heavily involved in conflicts in France, most notably the Hundred Years War, while the Kings of Scots were in an alliance with the French during this period.[57] + +The Bayeux Tapestry depicts the Battle of Hastings and the events leading to it. +The early modern period saw religious conflict resulting from the Reformation and the introduction of Protestant state churches in each country.[58] Wales was fully incorporated into the Kingdom of England,[59] and Ireland was constituted as a kingdom in personal union with the English crown.[60] In what was to become Northern Ireland, the lands of the independent Catholic Gaelic nobility were confiscated and given to Protestant settlers from England and Scotland.[61] +In 1603, the kingdoms of England, Scotland and Ireland were united in a personal union when James VI, King of Scots, inherited the crowns of England and Ireland and moved his court from Edinburgh to London; each country nevertheless remained a separate political entity and retained its separate political, legal, and religious institutions.[62][63] +In the mid-17th century, all three kingdoms were involved in a series of connected wars (including the English Civil War) which led to the temporary overthrow of the monarchy and the establishment of the short-lived unitary republic of the Commonwealth of England, Scotland and Ireland.[64][65] +Although the monarchy was restored, it ensured (with the Glorious Revolution of 1688) that, unlike much of the rest of Europe, royal absolutism would not prevail, and a professed Catholic could never accede to the throne. The British constitution would develop on the basis of constitutional monarchy and the parliamentary system.[66] During this period, particularly in England, the development of naval power (and the interest in voyages of discovery) led to the acquisition and settlement of overseas colonies, particularly in North America.[67][68] +Since the Acts of Union of 1707 +Main article: History of the United Kingdom + +The Treaty of Union led to a single united kingdom encompassing all Great Britain. +On 1 May 1707, the united kingdom of Great Britain came into being, the result of Acts of Union being passed by the parliaments of England and Scotland to ratify the 1706 Treaty of Union and so unite the two kingdoms.[69][70][71] +In the 18th century, cabinet government developed under Robert Walpole, in practice the first prime minister (1721–1742). A series of Jacobite Uprisings sought to remove the Protestant House of Hanover from the British throne and restore the Catholic House of Stuart. The Jacobites were finally defeated at the Battle of Culloden in 1746, after which the Scottish Highlanders were brutally suppressed. The British colonies in North America that broke away from Britain in the American War of Independence became the United States of America in 1782. British imperial ambition turned elsewhere, particularly to India.[72] +During the 18th century, Britain was involved in the Atlantic slave trade. British ships transported an estimated 2 million slaves from Africa to the West Indies before banning the trade in 1807.[73] The term 'United Kingdom' became official in 1801 when the parliaments of Britain and Ireland each passed an Act of Union, uniting the two kingdoms and creating the United Kingdom of Great Britain and Ireland.[74] +In the early 19th century, the British-led Industrial Revolution began to transform the country. It slowly led to a shift in political power away from the old Tory and Whig landowning classes towards the new industrialists. An alliance of merchants and industrialists with the Whigs would lead to a new party, the Liberals, with an ideology of free trade and laissez-faire. In 1832 Parliament passed the Great Reform Act, which began the transfer of political power from the aristocracy to the middle classes. In the countryside, enclosure of the land was driving small farmers out. Towns and cities began to swell with a new urban working class. Few ordinary workers had the vote, and they created their own organisations in the form of trade unions. +Painting of a bloody battle. Horses and infantry fight or lie on grass. +The Battle of Waterloo marked the end of the Napoleonic Wars and the start of Pax Britannica. +After the defeat of France in the Revolutionary and Napoleonic Wars (1792–1815), the UK emerged as the principal naval and imperial power of the 19th century (with London the largest city in the world from about 1830).[75] Unchallenged at sea, British dominance was later described as Pax Britannica.[76][77] By the time of the Great Exhibition of 1851, Britain was described as the "workshop of the world".[78] The British Empire was expanded to include India, large parts of Africa and many other territories throughout the world. Alongside the formal control it exerted over its own colonies, British dominance of much of world trade meant that it effectively controlled the economies of many countries, such as China, Argentina and Siam.[79][80] Domestically, political attitudes favoured free trade and laissez-faire policies and a gradual widening of the voting franchise. During the century, the population increased at a dramatic rate, accompanied by rapid urbanisation, causing significant social and economic stresses.[81] After 1875, the UK's industrial monopoly was challenged by Germany and the USA. To seek new markets and sources of raw materials, the Conservative Party under Disraeli launched a period of imperialist expansion in Egypt, South Africa and elsewhere. Canada, Australia and New Zealand became self-governing dominions.[82] +Social reform and home rule for Ireland were important domestic issues after 1900. The Labour Party emerged from an alliance of trade unions and small Socialist groups in 1900, and suffragettes campaigned for women's right to vote before 1914. +Black-and-white photo of two dozen men in military uniforms and metal helmets sitting or standing in a muddy trench. +Infantry of the Royal Irish Rifles during the Battle of the Somme. More than 885,000 British soldiers died on the battlefields of World War I. +The UK fought with France, Russia and (after 1917) the US, against Germany and its allies in World War I (1914–18).[83] The UK armed forces were engaged across much of the British Empire and in several regions of Europe, particularly on the Western front.[84] The high fatalities of trench warfare caused the loss of much of a generation of men, with lasting social effects in the nation and a great disruption in the social order. +After the war, the UK received the League of Nations mandate over a number of former German and Ottoman colonies. The British Empire reached its greatest extent, covering a fifth of the world's land surface and a quarter of its population.[85] However, the UK had suffered 2.5 million casualties and finished the war with a huge national debt.[84] The rise of Irish Nationalism and disputes within Ireland over the terms of Irish Home Rule led eventually to the partition of the island in 1921,[86] and the Irish Free State became independent with Dominion status in 1922. Northern Ireland remained part of the United Kingdom.[87] A wave of strikes in the mid-1920s culminated in the UK General Strike of 1926. The UK had still not recovered from the effects of the war when the Great Depression (1929–32) occurred. This led to considerable unemployment and hardship in the old industrial areas, as well as political and social unrest in the 1930s. A coalition government was formed in 1931.[88] +The UK entered World War II by declaring war on Germany in 1939, after it had invaded Poland and Czechoslovakia. In 1940, Winston Churchill became prime minister and head of a coalition government. Despite the defeat of its European allies in the first year of the war, the UK continued the fight alone against Germany. In 1940, the RAF defeated the German Luftwaffe in a struggle for control of the skies in the Battle of Britain. The UK suffered heavy bombing during the Blitz. There were also eventual hard-fought victories in the Battle of the Atlantic, the North Africa campaign and Burma campaign. UK forces played an important role in the Normandy landings of 1944, achieved with its ally the US. After Germany's defeat, the UK was one of the Big Three powers who met to plan the post-war world; it was an original signatory to the Declaration of the United Nations. The UK became one of the five permanent members of the United Nations Security Council. However, the war left the UK severely weakened and depending financially on Marshall Aid and loans from the United States.[89] +Map of the world. Canada, the eastern United States, countries in east Africa, India, most of Australasia and some other countries are highlighted in pink. +Territories that were at one time part of the British Empire. Current British Overseas Territories are underlined in red. +In the immediate post-war years, the Labour government initiated a radical programme of reforms, which had a significant effect on British society in the following decades.[90] Major industries and public utilities were nationalised, a Welfare State was established, and a comprehensive, publicly funded healthcare system, the National Health Service, was created.[91] The rise of nationalism in the colonies coincided with Britain's now much-diminished economic position, so that a policy of decolonisation was unavoidable. Independence was granted to India and Pakistan in 1947.[92] Over the next three decades, most colonies of the British Empire gained their independence. Many became members of the Commonwealth of Nations.[93] +Although the UK was the third country to develop a nuclear weapons arsenal (with its first atomic bomb test in 1952), the new post-war limits of Britain's international role were illustrated by the Suez Crisis of 1956. The international spread of the English language ensured the continuing international influence of its literature and culture. From the 1960s onward, its popular culture was also influential abroad. As a result of a shortage of workers in the 1950s, the UK government encouraged immigration from Commonwealth countries. In the following decades, the UK became a multi-ethnic society.[94] Despite rising living standards in the late 1950s and 1960s, the UK's economic performance was not as successful as many of its competitors, such as West Germany and Japan. In 1973, the UK joined the European Economic Community (EEC), and when the EEC became the European Union (EU) in 1992, it was one of the 12 founding members. + +After the two vetos of France in 1961 and 1967, the UK entered in the European Union in 1973. In 1975, 67% of Britons voted yes to the permanence in the European Union. +From the late 1960s, Northern Ireland suffered communal and paramilitary violence (sometimes affecting other parts of the UK) conventionally known as the Troubles. It is usually considered to have ended with the Belfast "Good Friday" Agreement of 1998.[95][96][97] +Following a period of widespread economic slowdown and industrial strife in the 1970s, the Conservative Government of the 1980s initiated a radical policy of monetarism, deregulation, particularly of the financial sector (for example, Big Bang in 1986) and labour markets, the sale of state-owned companies (privatisation), and the withdrawal of subsidies to others.[98] This resulted in high unemployment and social unrest, but ultimately also economic growth, particularly in the services sector. From 1984, the economy was helped by the inflow of substantial North Sea oil revenues.[99] +Around the end of the 20th century there were major changes to the governance of the UK with the establishment of devolved administrations for Scotland, Wales and Northern Ireland.[13][100] The statutory incorporation followed acceptance of the European Convention on Human Rights. The UK is still a key global player diplomatically and militarily. It plays leading roles in the EU, UN and NATO. However, controversy surrounds some of Britain's overseas military deployments, particularly in Afghanistan and Iraq.[101] +The 2008 global financial crisis severely affected the UK economy. The coalition government of 2010 introduced austerity measures intended to tackle the substantial public deficits which resulted.[102] In 2014 the Scottish Government held a referendum on Scottish independence, with the majority of voters rejecting the independence proposal and opting to remain within the United Kingdom.[103] +Geography +Main article: Geography of the United Kingdom +Map of United Kingdom showing hilly regions to north and west, and flattest region in the south-east. +The topography of the UK +The total area of the United Kingdom is approximately 243,610 square kilometres (94,060 sq mi). The country occupies the major part of the British Isles[104] archipelago and includes the island of Great Britain, the northeastern one-sixth of the island of Ireland and some smaller surrounding islands. It lies between the North Atlantic Ocean and the North Sea with the south-east coast coming within 22 miles (35 km) of the coast of northern France, from which it is separated by the English Channel.[105] In 1993 10% of the UK was forested, 46% used for pastures and 25% cultivated for agriculture.[106] The Royal Greenwich Observatory in London is the defining point of the Prime Meridian.[107] +The United Kingdom lies between latitudes 49° to 61° N, and longitudes 9° W to 2° E. Northern Ireland shares a 224-mile (360 km) land boundary with the Republic of Ireland.[105] The coastline of Great Britain is 11,073 miles (17,820 km) long.[108] It is connected to continental Europe by the Channel Tunnel, which at 31 miles (50 km) (24 miles (38 km) underwater) is the longest underwater tunnel in the world.[109] +England accounts for just over half of the total area of the UK, covering 130,395 square kilometres (50,350 sq mi).[110] Most of the country consists of lowland terrain,[106] with mountainous terrain north-west of the Tees-Exe line; including the Cumbrian Mountains of the Lake District, the Pennines and limestone hills of the Peak District, Exmoor and Dartmoor. The main rivers and estuaries are the Thames, Severn and the Humber. England's highest mountain is Scafell Pike (978 metres (3,209 ft)) in the Lake District. Its principal rivers are the Severn, Thames, Humber, Tees, Tyne, Tweed, Avon, Exe and Mersey.[106] +Scotland accounts for just under a third of the total area of the UK, covering 78,772 square kilometres (30,410 sq mi)[111] and including nearly eight hundred islands,[112] predominantly west and north of the mainland; notably the Hebrides, Orkney Islands and Shetland Islands. The topography of Scotland is distinguished by the Highland Boundary Fault – a geological rock fracture – which traverses Scotland from Arran in the west to Stonehaven in the east.[113] The faultline separates two distinctively different regions; namely the Highlands to the north and west and the lowlands to the south and east. The more rugged Highland region contains the majority of Scotland's mountainous land, including Ben Nevis which at 1,343 metres (4,406 ft) is the highest point in the British Isles.[114] Lowland areas – especially the narrow waist of land between the Firth of Clyde and the Firth of Forth known as the Central Belt – are flatter and home to most of the population including Glasgow, Scotland's largest city, and Edinburgh, its capital and political centre. +A view of Ben Nevis in the distance, fronted by rolling plains +Ben Nevis, in Scotland, is the highest point in the British Isles +Wales accounts for less than a tenth of the total area of the UK, covering 20,779 square kilometres (8,020 sq mi).[115] Wales is mostly mountainous, though South Wales is less mountainous than North and mid Wales. The main population and industrial areas are in South Wales, consisting of the coastal cities of Cardiff, Swansea and Newport, and the South Wales Valleys to their north. The highest mountains in Wales are in Snowdonia and include Snowdon (Welsh: Yr Wyddfa) which, at 1,085 metres (3,560 ft), is the highest peak in Wales.[106] The 14, or possibly 15, Welsh mountains over 3,000 feet (914 m) high are known collectively as the Welsh 3000s. Wales has over 2,704 kilometres (1,680 miles) of coastline.[116] Several islands lie off the Welsh mainland, the largest of which is Anglesey (Ynys Môn) in the northwest. +Northern Ireland, separated from Great Britain by the Irish Sea and North Channel, has an area of 14,160 square kilometres (5,470 sq mi) and is mostly hilly. It includes Lough Neagh which, at 388 square kilometres (150 sq mi), is the largest lake in the British Isles by area.[117] The highest peak in Northern Ireland is Slieve Donard in the Mourne Mountains at 852 metres (2,795 ft).[106] +Climate +Main article: Climate of the United Kingdom +The United Kingdom has a temperate climate, with plentiful rainfall all year round.[105] The temperature varies with the seasons seldom dropping below −11 °C (12 °F) or rising above 35 °C (95 °F).[118] The prevailing wind is from the south-west and bears frequent spells of mild and wet weather from the Atlantic Ocean,[105] although the eastern parts are mostly sheltered from this wind since the majority of the rain falls over the western regions the eastern parts are therefore the driest. Atlantic currents, warmed by the Gulf Stream, bring mild winters; especially in the west where winters are wet and even more so over high ground. Summers are warmest in the south-east of England, being closest to the European mainland, and coolest in the north. Heavy snowfall can occur in winter and early spring on high ground, and occasionally settles to great depth away from the hills. +Administrative divisions +Main article: Administrative geography of the United Kingdom +Each country of the United Kingdom has its own system of administrative and geographic demarcation, whose origins often pre-date the formation of the United Kingdom. Thus there is "no common stratum of administrative unit encompassing the United Kingdom".[119] Until the 19th century there was little change to those arrangements, but there has since been a constant evolution of role and function.[120] Change did not occur in a uniform manner and the devolution of power over local government to Scotland, Wales and Northern Ireland means that future changes are unlikely to be uniform either. +The organisation of local government in England is complex, with the distribution of functions varying according to local arrangements. Legislation concerning local government in England is the responsibility of the UK parliament and the Government of the United Kingdom, as England has no devolved parliament. The upper-tier subdivisions of England are the nine Government office regions or European Union government office regions.[121] One region, Greater London, has had a directly elected assembly and mayor since 2000 following popular support for the proposal in a referendum.[122] It was intended that other regions would also be given their own elected regional assemblies, but a proposed assembly in the North East region was rejected by a referendum in 2004.[123] Below the regional tier, some parts of England have county councils and district councils and others have unitary authorities; while London consists of 32 London boroughs and the City of London. Councillors are elected by the first-past-the-post system in single-member wards or by the multi-member plurality system in multi-member wards.[124] +For local government purposes, Scotland is divided into 32 council areas, with wide variation in both size and population. The cities of Glasgow, Edinburgh, Aberdeen and Dundee are separate council areas, as is the Highland Council which includes a third of Scotland's area but only just over 200,000 people. Local councils are made up of elected councillors, of whom there are currently 1,222;[125] they are paid a part-time salary. Elections are conducted by single transferable vote in multi-member wards that elect either three or four councillors. Each council elects a Provost, or Convenor, to chair meetings of the council and to act as a figurehead for the area. Councillors are subject to a code of conduct enforced by the Standards Commission for Scotland.[126] The representative association of Scotland's local authorities is the Convention of Scottish Local Authorities (COSLA).[127] +Local government in Wales consists of 22 unitary authorities. These include the cities of Cardiff, Swansea and Newport which are unitary authorities in their own right.[128] Elections are held every four years under the first-past-the-post system.[129] The most recent elections were held in May 2012, except for the Isle of Anglesey. The Welsh Local Government Association represents the interests of local authorities in Wales.[130] +Local government in Northern Ireland has since 1973 been organised into 26 district councils, each elected by single transferable vote. Their powers are limited to services such as collecting waste, controlling dogs and maintaining parks and cemeteries.[131] On 13 March 2008 the executive agreed on proposals to create 11 new councils and replace the present system.[132] The next local elections were postponed until 2016 to facilitate this.[133] +Dependencies + +A view of the Caribbean Sea from the Cayman Islands, one of the world's foremost international financial centres[134] and tourist destinations.[135] +Main articles: British Overseas Territories, Crown dependencies and British Islands +The United Kingdom has sovereignty over seventeen territories which do not form part of the United Kingdom itself: fourteen British Overseas Territories[136] and three Crown dependencies.[137] +The fourteen British Overseas Territories are: Anguilla; Bermuda; the British Antarctic Territory; the British Indian Ocean Territory; the British Virgin Islands; the Cayman Islands; the Falkland Islands; Gibraltar; Montserrat; Saint Helena, Ascension and Tristan da Cunha; the Turks and Caicos Islands; the Pitcairn Islands; South Georgia and the South Sandwich Islands; and Sovereign Base Areas on Cyprus.[138] British claims in Antarctica are not universally recognised.[139] Collectively Britain's overseas territories encompass an approximate land area of 1,727,570 square kilometres (667,018 sq mi) and a population of approximately 260,000 people.[140] They are the remnants of the British Empire and several have specifically voted to remain British territories (Bermuda in 1995, Gibraltar in 2002 and the Falkland Islands in 2013).[141] +The Crown dependencies are possessions of the Crown, as opposed to overseas territories of the UK.[142] They comprise three independently administered jurisdictions: the Channel Islands of Jersey and Guernsey in the English Channel, and the Isle of Man in the Irish Sea. By mutual agreement, the British Government manages the islands' foreign affairs and defence and the UK Parliament has the authority to legislate on their behalf. However, internationally, they are regarded as "territories for which the United Kingdom is responsible".[143] The power to pass legislation affecting the islands ultimately rests with their own respective legislative assemblies, with the assent of the Crown (Privy Council or, in the case of the Isle of Man, in certain circumstances the Lieutenant-Governor).[144] Since 2005 each Crown dependency has had a Chief Minister as its head of government.[145] +Politics +Main articles: Politics of the United Kingdom, Monarchy of the United Kingdom and Elections in the United Kingdom +Elderly lady with a yellow hat and grey hair is smiling in outdoor setting. +Elizabeth II, Queen of the United Kingdom and the other Commonwealth realms +The United Kingdom is a unitary state under a constitutional monarchy. Queen Elizabeth II is the head of state of the UK as well as monarch of fifteen other independent Commonwealth countries. The monarch has "the right to be consulted, the right to encourage, and the right to warn".[146] The United Kingdom is one of only four countries in the world to have an uncodified constitution.[147][nb 7] The Constitution of the United Kingdom thus consists mostly of a collection of disparate written sources, including statutes, judge-made case law and international treaties, together with constitutional conventions. As there is no technical difference between ordinary statutes and "constitutional law", the UK Parliament can perform "constitutional reform" simply by passing Acts of Parliament, and thus has the political power to change or abolish almost any written or unwritten element of the constitution. However, no Parliament can pass laws that future Parliaments cannot change.[148] +Government +Main article: Government of the United Kingdom +The UK has a parliamentary government based on the Westminster system that has been emulated around the world: a legacy of the British Empire. The parliament of the United Kingdom that meets in the Palace of Westminster has two houses; an elected House of Commons and an appointed House of Lords. All bills passed are given Royal Assent before becoming law. +The position of prime minister,[nb 8] the UK's head of government,[149] belongs to the person most likely to command the confidence of the House of Commons; this individual is typically the leader of the political party or coalition of parties that holds the largest number of seats in that chamber. The prime minister chooses a cabinet and they are formally appointed by the monarch to form Her Majesty's Government. By convention, the Queen respects the prime minister's decisions of government.[150] +Large sand-coloured building of Gothic design beside brown river and road bridge. The building has several large towers, including large clock-tower. +The Palace of Westminster, seat of both houses of the Parliament of the United Kingdom +The cabinet is traditionally drawn from members of a prime minister's party or coalition and mostly from the House of Commons but always from both legislative houses, the cabinet being responsible to both. Executive power is exercised by the prime minister and cabinet, all of whom are sworn into the Privy Council of the United Kingdom, and become Ministers of the Crown. The current Prime Minister is David Cameron, who has been in office since 11 May 2010.[151] Cameron is the leader of the Conservative Party and heads a coalition with the Liberal Democrats. For elections to the House of Commons, the UK is currently divided into 650 constituencies,[152] each electing a single member of parliament (MP) by simple plurality. General elections are called by the monarch when the prime minister so advises. The Parliament Acts 1911 and 1949 require that a new election must be called no later than five years after the previous general election.[153] +The UK's three major political parties are the Conservative Party (Tories), the Labour Party and the Liberal Democrats, representing the British traditions of conservatism, socialism and social liberalism, respectively. During the 2010 general election these three parties won 622 out of 650 seats available in the House of Commons.[154][155] Most of the remaining seats were won by parties that contest elections only in one part of the UK: the Scottish National Party (Scotland only); Plaid Cymru (Wales only); and the Alliance Party, Democratic Unionist Party, Social Democratic and Labour Party and Sinn Féin (Northern Ireland only[nb 9]). In accordance with party policy, no elected Sinn Féin members of parliament have ever attended the House of Commons to speak on behalf of their constituents because of the requirement to take an oath of allegiance to the monarch. +Devolved administrations +Main articles: Devolution in the United Kingdom, Northern Ireland Executive, Scottish Government and Welsh Government +Modern one-story building with grass on roof and large sculpted grass area in front. Behind are residential buildings in a mixture of styles. +The Scottish Parliament Building in Holyrood is the seat of the Scottish Parliament. +Scotland, Wales and Northern Ireland each have their own government or executive, led by a First Minister (or, in the case of Northern Ireland, a diarchal First Minister and deputy First Minister), and a devolved unicameral legislature. England, the largest country of the United Kingdom, has no such devolved executive or legislature and is administered and legislated for directly by the UK government and parliament on all issues. This situation has given rise to the so-called West Lothian question which concerns the fact that members of parliament from Scotland, Wales and Northern Ireland can vote, sometimes decisively,[156] on matters that only affect England.[157] The McKay Commission reported on this matter in March 2013 recommending that laws affecting only England should need support from a majority of English members of parliament.[158] +The Scottish Government and Parliament have wide-ranging powers over any matter that has not been specifically reserved to the UK parliament, including education, healthcare, Scots law and local government.[159] At the 2011 elections the Scottish National Party won re-election and achieved an overall majority in the Scottish parliament, with its leader, Alex Salmond, as First Minister of Scotland.[160][161] In 2012, the UK and Scottish governments signed the Edinburgh Agreement setting out the terms for a referendum on Scottish independence in 2014, which was defeated 55% to 45%. +The Welsh Government and the National Assembly for Wales have more limited powers than those devolved to Scotland.[162] The Assembly is able to legislate on devolved matters through Acts of the Assembly, which require no prior consent from Westminster. The 2011 elections resulted in a minority Labour administration led by Carwyn Jones.[163] +The Northern Ireland Executive and Assembly have powers similar to those devolved to Scotland. The Executive is led by a diarchy representing unionist and nationalist members of the Assembly. Currently, Peter Robinson (Democratic Unionist Party) and Martin McGuinness (Sinn Féin) are First Minister and deputy First Minister respectively.[164] Devolution to Northern Ireland is contingent on participation by the Northern Ireland administration in the North-South Ministerial Council, where the Northern Ireland Executive cooperates and develops joint and shared policies with the Government of Ireland. The British and Irish governments co-operate on non-devolved matters affecting Northern Ireland through the British–Irish Intergovernmental Conference, which assumes the responsibilities of the Northern Ireland administration in the event of its non-operation. +The UK does not have a codified constitution and constitutional matters are not among the powers devolved to Scotland, Wales or Northern Ireland. Under the doctrine of parliamentary sovereignty, the UK Parliament could, in theory, therefore, abolish the Scottish Parliament, Welsh Assembly or Northern Ireland Assembly.[165][166] Indeed, in 1972, the UK Parliament unilaterally prorogued the Parliament of Northern Ireland, setting a precedent relevant to contemporary devolved institutions.[167] In practice, it would be politically difficult for the UK Parliament to abolish devolution to the Scottish Parliament and the Welsh Assembly, given the political entrenchment created by referendum decisions.[168] The political constraints placed upon the UK Parliament's power to interfere with devolution in Northern Ireland are even greater than in relation to Scotland and Wales, given that devolution in Northern Ireland rests upon an international agreement with the Government of Ireland.[169] +Law and criminal justice +Main article: Law of the United Kingdom + +The Royal Courts of Justice of England and Wales +The United Kingdom does not have a single legal system, as Article 19 of the 1706 Treaty of Union provided for the continuation of Scotland's separate legal system.[170] Today the UK has three distinct systems of law: English law, Northern Ireland law and Scots law. A new Supreme Court of the United Kingdom came into being in October 2009 to replace the Appellate Committee of the House of Lords.[171][172] The Judicial Committee of the Privy Council, including the same members as the Supreme Court, is the highest court of appeal for several independent Commonwealth countries, the British Overseas Territories and the Crown Dependencies.[173] +Both English law, which applies in England and Wales, and Northern Ireland law are based on common-law principles.[174] The essence of common law is that, subject to statute, the law is developed by judges in courts, applying statute, precedent and common sense to the facts before them to give explanatory judgements of the relevant legal principles, which are reported and binding in future similar cases (stare decisis).[175] The courts of England and Wales are headed by the Senior Courts of England and Wales, consisting of the Court of Appeal, the High Court of Justice (for civil cases) and the Crown Court (for criminal cases). The Supreme Court is the highest court in the land for both criminal and civil appeal cases in England, Wales and Northern Ireland and any decision it makes is binding on every other court in the same jurisdiction, often having a persuasive effect in other jurisdictions.[176] + +The High Court of Justiciary – the supreme criminal court of Scotland. +Scots law is a hybrid system based on both common-law and civil-law principles. The chief courts are the Court of Session, for civil cases,[177] and the High Court of Justiciary, for criminal cases.[178] The Supreme Court of the United Kingdom serves as the highest court of appeal for civil cases under Scots law.[179] Sheriff courts deal with most civil and criminal cases including conducting criminal trials with a jury, known as sheriff solemn court, or with a sheriff and no jury, known as sheriff summary Court.[180] The Scots legal system is unique in having three possible verdicts for a criminal trial: "guilty", "not guilty" and "not proven". Both "not guilty" and "not proven" result in an acquittal.[181] +Crime in England and Wales increased in the period between 1981 and 1995, though since that peak there has been an overall fall of 48% in crime from 1995 to 2007/08,[182] according to crime statistics. The prison population of England and Wales has almost doubled over the same period, to over 80,000, giving England and Wales the highest rate of incarceration in Western Europe at 147 per 100,000.[183] Her Majesty's Prison Service, which reports to the Ministry of Justice, manages most of the prisons within England and Wales. Crime in Scotland fell to its lowest recorded level for 32 years in 2009/10, falling by ten per cent.[184] At the same time Scotland's prison population, at over 8,000,[185] is at record levels and well above design capacity.[186] The Scottish Prison Service, which reports to the Cabinet Secretary for Justice, manages Scotland's prisons. +Foreign relations +Main article: Foreign relations of the United Kingdom + +The Prime Minister of the United Kingdom, David Cameron, and the President of the United States, Barack Obama, during the 2010 G-20 Toronto summit. +The UK is a permanent member of the United Nations Security Council, a member of NATO, the Commonwealth of Nations, G7, G8, G20, the OECD, the WTO, the Council of Europe, the OSCE, and is a member state of the European Union. The UK is said to have a "Special Relationship" with the United States and a close partnership with France—the "Entente cordiale"—and shares nuclear weapons technology with both countries.[187][188] The UK is also closely linked with the Republic of Ireland; the two countries share a Common Travel Area and co-operate through the British-Irish Intergovernmental Conference and the British-Irish Council. Britain's global presence and influence is further amplified through its trading relations, foreign investments, official development assistance and military engagements.[189] +Military + +Troopers of the Blues and Royals during the 2007 Trooping the Colour ceremony +Main article: British Armed Forces +The armed forces of the United Kingdom—officially, Her Majesty's Armed Forces—consist of three professional service branches: the Royal Navy and Royal Marines (forming the Naval Service), the British Army and the Royal Air Force.[190] The forces are managed by the Ministry of Defence and controlled by the Defence Council, chaired by the Secretary of State for Defence. The Commander-in-Chief is the British monarch, Elizabeth II, to whom members of the forces swear an oath of allegiance.[191] The Armed Forces are charged with protecting the UK and its overseas territories, promoting the UK's global security interests and supporting international peacekeeping efforts. They are active and regular participants in NATO, including the Allied Rapid Reaction Corps, as well as the Five Power Defence Arrangements, RIMPAC and other worldwide coalition operations. Overseas garrisons and facilities are maintained in Ascension Island, Belize, Brunei, Canada, Cyprus, Diego Garcia, the Falkland Islands, Germany, Gibraltar, Kenya and Qatar.[192] +The British armed forces played a key role in establishing the British Empire as the dominant world power in the 18th, 19th and early 20th centuries. Throughout its unique history the British forces have seen action in a number of major wars, such as the Seven Years' War, the Napoleonic Wars, the Crimean War, World War I and World War II—as well as many colonial conflicts. By emerging victorious from such conflicts, Britain has often been able to decisively influence world events. Since the end of the British Empire, the UK has nonetheless remained a major military power. Following the end of the Cold War, defence policy has a stated assumption that "the most demanding operations" will be undertaken as part of a coalition.[193] Setting aside the intervention in Sierra Leone, recent UK military operations in Bosnia, Kosovo, Afghanistan, Iraq and, most recently, Libya, have followed this approach. The last time the British military fought alone was the Falklands War of 1982. +According to various sources, including the Stockholm International Peace Research Institute and the International Institute for Strategic Studies, the United Kingdom has the fifth- or sixth-highest military expenditure in the world. Total defence spending currently accounts for around 2.4% of total national GDP.[22][23] +Economy +Main article: Economy of the United Kingdom + +The Bank of England – the central bank of the United Kingdom +The UK has a partially regulated market economy.[194] Based on market exchange rates the UK is today the sixth-largest economy in the world and the third-largest in Europe after Germany and France, having fallen behind France for the first time in over a decade in 2008.[195] HM Treasury, led by the Chancellor of the Exchequer, is responsible for developing and executing the British government's public finance policy and economic policy. The Bank of England is the UK's central bank and is responsible for issuing notes and coins in the nation's currency, the pound sterling. Banks in Scotland and Northern Ireland retain the right to issue their own notes, subject to retaining enough Bank of England notes in reserve to cover their issue. Pound sterling is the world's third-largest reserve currency (after the US Dollar and the Euro).[196] Since 1997 the Bank of England's Monetary Policy Committee, headed by the Governor of the Bank of England, has been responsible for setting interest rates at the level necessary to achieve the overall inflation target for the economy that is set by the Chancellor each year.[197] +The UK service sector makes up around 73% of GDP.[198] London is one of the three "command centres" of the global economy (alongside New York City and Tokyo),[199] it is the world's largest financial centre alongside New York,[200][201][202] and it has the largest city GDP in Europe.[203] Edinburgh is also one of the largest financial centres in Europe.[204] Tourism is very important to the British economy and, with over 27 million tourists arriving in 2004, the United Kingdom is ranked as the sixth major tourist destination in the world and London has the most international visitors of any city in the world.[205][206] The creative industries accounted for 7% GVA in 2005 and grew at an average of 6% per annum between 1997 and 2005.[207] + +The Airbus A350 has its wings and engines manufactured in the UK. +The Industrial Revolution started in the UK with an initial concentration on the textile industry,[208] followed by other heavy industries such as shipbuilding, coal mining and steelmaking.[209][210] +The empire was exploited as an overseas market for British products, allowing the UK to dominate international trade in the 19th century. As other nations industrialised, coupled with economic decline after two world wars, the United Kingdom began to lose its competitive advantage and heavy industry declined, by degrees, throughout the 20th century. Manufacturing remains a significant part of the economy but accounted for only 16.7% of national output in 2003.[211] +The automotive industry is a significant part of the UK manufacturing sector and employs over 800,000 people, with a turnover of some £52 billion, generating £26.6 billion of exports.[212] +The aerospace industry of the UK is the second- or third-largest national aerospace industry in the world depending upon the method of measurement and has an annual turnover of around £20 billion. The wings for the Airbus A380 and the A350 XWB are designed and manufactured at Airbus UK's world-leading Broughton facility, whilst over a quarter of the value of the Boeing 787 comes from UK manufacturers including Eaton (fuel subsystem pumps), Messier-Bugatti-Dowty (the landing gear) and Rolls-Royce (the engines). Other key names include GKN Aerospace – an expert in metallic and composite aerostructures that's involved in almost every civil and military fixed and rotary wing aircraft in production and development today.[213][214][215][216] +BAE Systems - plays a critical role on some of the world's biggest defence aerospace projects. The company makes large sections of the Typhoon Eurofighter at its sub-assembly plant in Salmesbury and assembles the aircraft for the RAF at its Warton Plant, near Preston. It is also a principal subcontractor on the F35 Joint Strike Fighter - the world's largest single defence project - for which it designs and manufactures a range of components including the aft fuselage, vertical and horizontal tail and wing tips and fuel system. As well as this it manufactures the Hawk, the world's most successful jet training aircraft.[216] Airbus UK also manufactures the wings for the A400m military transporter. Rolls-Royce, is the world's second-largest aero-engine manufacturer. Its engines power more than 30 types of commercial aircraft and it has more than 30,000 engines currently in service across both the civil and defence sectors. Agusta Westland designs and manufactures complete helicopters in the UK.[216] +The UK space industry is growing very fast. Worth £9.1bn in 2011 and employing 29,000 people, it is growing at a rate of some 7.5 per cent annually, according to its umbrella organisation, the UK Space Agency. Government strategy is for the space industry to be a £40bn business for the UK by 2030, capturing a 10 per cent share of the $250bn world market for commercial space technology.[216] On 16 July 2013, the British government pledged £60m to the Skylon project: this investment will provide support at a "crucial stage" to allow a full-scale prototype of the SABRE engine to be built. +The pharmaceutical industry plays an important role in the UK economy and the country has the third-highest share of global pharmaceutical R&D expenditures (after the United States and Japan).[217][218] +Agriculture is intensive, highly mechanised and efficient by European standards, producing about 60% of food needs with less than 1.6% of the labour force (535,000 workers).[219] Around two-thirds of production is devoted to livestock, one-third to arable crops. Farmers are subsidised by the EU's Common Agricultural Policy. The UK retains a significant, though much reduced fishing industry. It is also rich in a number of natural resources including coal, petroleum, natural gas, tin, limestone, iron ore, salt, clay, chalk, gypsum, lead, silica and an abundance of arable land. + +The City of London is the world's largest financial centre alongside New York[200][201][202] +In the final quarter of 2008 the UK economy officially entered recession for the first time since 1991.[220] Unemployment increased from 5.2% in May 2008 to 7.6% in May 2009 and by January 2012 the unemployment rate among 18 to 24-year-olds had risen from 11.9% to 22.5%, the highest since current records began in 1992.[221][222] Total UK government debt rose from 44.4% of GDP in 2007 to 82.9% of GDP in 2011.[223] In February 2013, the UK lost its top AAA credit rating for the first time since 1978.[224] +Inflation-adjusted wages in the UK fell by 3.2% between the third quarter of 2010 and the third quarter of 2012.[225] Since the 1980s, economic inequality has grown faster in the UK than in any other developed country.[226] +The poverty line in the UK is commonly defined as being 60% of the median household income.[nb 10] In 2007–2008 13.5 million people, or 22% of the population, lived below this line. This is a higher level of relative poverty than all but four other EU members.[227] In the same year 4.0 million children, 31% of the total, lived in households below the poverty line after housing costs were taken into account. This is a decrease of 400,000 children since 1998–1999.[228] The UK imports 40% of its food supplies.[229] The Office for National Statistics has estimated that in 2011, 14 million people were at risk of poverty or social exclusion, and that one person in 20 (5.1%) was now experiencing "severe material depression,"[230] up from 3 million people in 1977.[231][232] +Science and technology +Main article: Science and technology in the United Kingdom + +Charles Darwin (1809–82), whose theory of evolution by natural selection is the foundation of modern biological sciences +England and Scotland were leading centres of the Scientific Revolution from the 17th century[233] and the United Kingdom led the Industrial Revolution from the 18th century,[208] and has continued to produce scientists and engineers credited with important advances.[234] Major theorists from the 17th and 18th centuries include Isaac Newton, whose laws of motion and illumination of gravity have been seen as a keystone of modern science;[235] from the 19th century Charles Darwin, whose theory of evolution by natural selection was fundamental to the development of modern biology, and James Clerk Maxwell, who formulated classical electromagnetic theory; and more recently Stephen Hawking, who has advanced major theories in the fields of cosmology, quantum gravity and the investigation of black holes.[236] Major scientific discoveries from the 18th century include hydrogen by Henry Cavendish;[237] from the 20th century penicillin by Alexander Fleming,[238] and the structure of DNA, by Francis Crick and others.[239] Major engineering projects and applications by people from the UK in the 18th century include the steam locomotive, developed by Richard Trevithick and Andrew Vivian;[240] from the 19th century the electric motor by Michael Faraday, the incandescent light bulb by Joseph Swan,[241] and the first practical telephone, patented by Alexander Graham Bell;[242] and in the 20th century the world's first working television system by John Logie Baird and others,[243] the jet engine by Frank Whittle, the basis of the modern computer by Alan Turing, and the World Wide Web by Tim Berners-Lee.[244] +Scientific research and development remains important in British universities, with many establishing science parks to facilitate production and co-operation with industry.[245] Between 2004 and 2008 the UK produced 7% of the world's scientific research papers and had an 8% share of scientific citations, the third and second highest in the world (after the United States and China, and the United States, respectively).[246] Scientific journals produced in the UK include Nature, the British Medical Journal and The Lancet.[247] +Transport +Main article: Transport in the United Kingdom + +Heathrow Terminal 5 building. London Heathrow Airport has the most international passenger traffic of any airport in the world.[248][249] +A radial road network totals 29,145 miles (46,904 km) of main roads, 2,173 miles (3,497 km) of motorways and 213,750 miles (344,000 km) of paved roads.[105] In 2009 there were a total of 34 million licensed vehicles in Great Britain.[250] +The UK has a railway network of 10,072 miles (16,209 km) in Great Britain and 189 miles (304 km) in Northern Ireland. Railways in Northern Ireland are operated by NI Railways, a subsidiary of state-owned Translink. In Great Britain, the British Rail network was privatised between 1994 and 1997. Network Rail owns and manages most of the fixed assets (tracks, signals etc.). About 20 privately owned (and foreign state-owned railways including: Deutsche Bahn; SNCF and Nederlandse Spoorwegen) Train Operating Companies (including state-owned East Coast), operate passenger trains and carry over 18,000 passenger trains daily. There are also some 1,000 freight trains in daily operation.[105] The UK government is to spend £30 billion on a new high-speed railway line, HS2, to be operational by 2025.[251] Crossrail, under construction in London, Is Europe's largest construction project with a £15 billion projected cost.[252][253] +In the year from October 2009 to September 2010 UK airports handled a total of 211.4 million passengers.[254] In that period the three largest airports were London Heathrow Airport (65.6 million passengers), Gatwick Airport (31.5 million passengers) and London Stansted Airport (18.9 million passengers).[254] London Heathrow Airport, located 15 miles (24 km) west of the capital, has the most international passenger traffic of any airport in the world[248][249] and is the hub for the UK flag carrier British Airways, as well as for BMI and Virgin Atlantic.[255] +Energy +Main article: Energy in the United Kingdom + +An oil platform in the North Sea +In 2006, the UK was the world's ninth-largest consumer of energy and the 15th-largest producer.[256] The UK is home to a number of large energy companies, including two of the six oil and gas "supermajors" – BP and Royal Dutch Shell – and BG Group.[257][258] In 2011, 40% of the UK's electricity was produced by gas, 30% by coal, 19% by nuclear power and 4.2% by wind, hydro, biofuels and wastes.[259] +In 2009, the UK produced 1.5 million barrels per day (bbl/d) of oil and consumed 1.7 million bbl/d.[260] Production is now in decline and the UK has been a net importer of oil since 2005.[260] In 2010 the UK had around 3.1 billion barrels of proven crude oil reserves, the largest of any EU member state.[260] In 2009, 66.5% of the UK's oil supply was imported.[261] +In 2009, the UK was the 13th-largest producer of natural gas in the world and the largest producer in the EU.[262] Production is now in decline and the UK has been a net importer of natural gas since 2004.[262] In 2009, half of British gas was supplied from imports and this is expected to increase to at least 75% by 2015, as domestic reserves are depleted.[259] +Coal production played a key role in the UK economy in the 19th and 20th centuries. In the mid-1970s, 130 million tonnes of coal was being produced annually, not falling below 100 million tonnes until the early 1980s. During the 1980s and 1990s the industry was scaled back considerably. In 2011, the UK produced 18.3 million tonnes of coal.[263] In 2005 it had proven recoverable coal reserves of 171 million tons.[263] The UK Coal Authority has stated there is a potential to produce between 7 billion tonnes and 16 billion tonnes of coal through underground coal gasification (UCG) or 'fracking',[264] and that, based on current UK coal consumption, such reserves could last between 200 and 400 years.[265] However, environmental and social concerns have been raised over chemicals getting into the water table and minor earthquakes damaging homes.[266][267] +In the late 1990s, nuclear power plants contributed around 25% of total annual electricity generation in the UK, but this has gradually declined as old plants have been shut down and ageing-related problems affect plant availability. In 2012, the UK had 16 reactors normally generating about 19% of its electricity. All but one of the reactors will be retired by 2023. Unlike Germany and Japan, the UK intends to build a new generation of nuclear plants from about 2018.[259] +Demographics +Main article: Demographics of the United Kingdom + +Map of population density in the UK as at the 2011 census. +A census is taken simultaneously in all parts of the UK every ten years.[268] The Office for National Statistics is responsible for collecting data for England and Wales, the General Register Office for Scotland and the Northern Ireland Statistics and Research Agency each being responsible for censuses in their respective countries.[269] In the 2011 census the total population of the United Kingdom was 63,181,775.[270] It is the third-largest in the European Union, the fifth-largest in the Commonwealth and the 21st-largest in the world. 2010 was the third successive year in which natural change contributed more to population growth than net long-term international migration.[271][271] Between 2001 and 2011 the population increased by an average annual rate of approximately 0.7 per cent.[270] This compares to 0.3 per cent per year in the period 1991 to 2001 and 0.2 per cent in the decade 1981 to 1991.[271] The 2011 census also confirmed that the proportion of the population aged 0–14 has nearly halved (31 per cent in 1911 compared to 18 in 2011) and the proportion of older people aged 65 and over has more than trebled (from 5 to 16 per cent).[270] It has been estimated that the number of people aged 100 or over will rise steeply to reach over 626,000 by 2080.[272] +England's population in 2011 was found to be 53 million.[273] It is one of the most densely populated countries in the world, with 383 people resident per square kilometre in mid-2003,[274] with a particular concentration in London and the south-east.[275] The 2011 census put Scotland's population at 5.3 million,[276] Wales at 3.06 million and Northern Ireland at 1.81 million.[273] In percentage terms England has had the fastest growing population of any country of the UK in the period from 2001 to 2011, with an increase of 7.9%. +In 2012 the average total fertility rate (TFR) across the UK was 1.92 children per woman.[277] While a rising birth rate is contributing to current population growth, it remains considerably below the 'baby boom' peak of 2.95 children per woman in 1964,[278] below the replacement rate of 2.1, but higher than the 2001 record low of 1.63.[277] In 2012, Scotland had the lowest TFR at only 1.67, followed by Wales at 1.88, England at 1.94, and Northern Ireland at 2.03.[277] In 2011, 47.3% of births in the UK were to unmarried women.[279] A government figure estimated that there are 3.6 million homosexual people in Britain comprising 6 per cent of the population.[280] +view talk edit +view talk edit +Largest urban areas of the United Kingdom +United Kingdom 2011 census Built-up areas[281][282][283] +Rank Urban area Pop. Principal settlement Rank Urban area Pop. Principal settlement +Greater London Urban Area +Greater London Urban Area +Greater Manchester Urban Area +Greater Manchester Urban Area +1 Greater London Urban Area 9,787,426 London 11 Bristol Urban Area 617,280 Bristol West Midlands Urban Area +West Midlands Urban Area +West Yorkshire Urban Area +West Yorkshire Urban Area +2 Greater Manchester Urban Area 2,553,379 Manchester 12 Belfast Metropolitan Urban Area 579,236 Belfast +3 West Midlands Urban Area 2,440,986 Birmingham 13 Leicester Urban Area 508,916 Leicester +4 West Yorkshire Urban Area 1,777,934 Leeds 14 Edinburgh 488,610 Edinburgh +5 Greater Glasgow 976,970 Glasgow 15 Brighton/Worthing/Littlehampton 474,485 Brighton +6 Liverpool Urban Area 864,122 Liverpool 16 South East Dorset conurbation 466,266 Bournemouth +7 South Hampshire 855,569 Southampton 17 Cardiff Urban Area 390,214 Cardiff +8 Tyneside 774,891 Newcastle 18 Teesside 376,633 Middlesbrough +9 Nottingham Urban Area 729,977 Nottingham 19 The Potteries Urban Area 372,775 Stoke-on-Trent +10 Sheffield Urban Area 685,368 Sheffield 20 Coventry and Bedworth Urban Area 359,262 Coventry + +Ethnic groups + +Map showing the percentage of the population who are not white according to the 2011 census. +Ethnic group 2011 +population 2011 +% +White 55,010,359 87.1 +White: Irish Traveller 63,193 0.1 +Asian or Asian British: Indian 1,451,862 +2.3 +Asian or Asian British: Pakistani 1,173,892 +1.9 +Asian or Asian British: Bangladeshi 451,529 +0.7 +Asian or Asian British: Chinese 433,150 +0.7 +Asian or Asian British: Asian Other 861,815 +1.4 +Asian or Asian British: Total 4,373,339 +7.0 +Black or Black British 1,904,684 +3.0 +British Mixed 1,250,229 +2.0 +Other: Total 580,374 +0.9 +Total[284] 63,182,178 +100 +Historically, indigenous British people were thought to be descended from the various ethnic groups that settled there before the 11th century: the Celts, Romans, Anglo-Saxons, Norse and the Normans. Welsh people could be the oldest ethnic group in the UK.[285] A 2006 genetic study shows that more than 50 per cent of England's gene pool contains Germanic Y chromosomes.[286] Another 2005 genetic analysis indicates that "about 75 per cent of the traceable ancestors of the modern British population had arrived in the British isles by about 6,200 years ago, at the start of the British Neolithic or Stone Age", and that the British broadly share a common ancestry with the Basque people.[287][288][289] +The UK has a history of small-scale non-white immigration, with Liverpool having the oldest Black population in the country dating back to at least the 1730s during the period of the African slave trade,[290] and the oldest Chinese community in Europe, dating to the arrival of Chinese seamen in the 19th century.[291] In 1950 there were probably fewer than 20,000 non-white residents in Britain, almost all born overseas.[292] +Since 1948 substantial immigration from Africa, the Caribbean and South Asia has been a legacy of ties forged by the British Empire. Migration from new EU member states in Central and Eastern Europe since 2004 has resulted in growth in these population groups but, as of 2008, the trend is reversing. Many of these migrants are returning to their home countries, leaving the size of these groups unknown.[293] In 2011, 86% of the population identified themselves as White, meaning 12.9% of the UK population identify themselves as of mixed ethnic minority. +Ethnic diversity varies significantly across the UK. 30.4% of London's population and 37.4% of Leicester's was estimated to be non-white in 2005,[294][295] whereas less than 5% of the populations of North East England, Wales and the South West were from ethnic minorities, according to the 2001 census.[296] In 2011, 26.5% of primary and 22.2% of secondary pupils at state schools in England were members of an ethnic minority.[297] +The non-white British population of England and Wales increased by 38% from 6.6 million in 2001 to 9.1 million in 2009.[298] The fastest-growing group was the mixed-ethnicity population, which doubled from 672,000 in 2001 to 986,600 in 2009. Also in the same period, a decrease of 36,000 white British people was recorded.[299] +Languages +Main article: Languages of the United Kingdom + +The English-speaking world. Countries in dark blue have a majority of native speakers; countries where English is an official but not a majority language are shaded in light blue. English is one of the official languages of the European Union[300] and the United Nations[301] +The UK's de facto official language is English.[302][303] It is estimated that 95% of the UK's population are monolingual English speakers.[304] 5.5% of the population are estimated to speak languages brought to the UK as a result of relatively recent immigration.[304] South Asian languages, including Bengali, Tamil, Punjabi, Hindi and Gujarati, are the largest grouping and are spoken by 2.7% of the UK population.[304] According to the 2011 census, Polish has become the second-largest language spoken in England and has 546,000 speakers.[305] +Four Celtic languages are spoken in the UK: Welsh; Irish; Scottish Gaelic; and Cornish. All are recognised as regional or minority languages, subject to specific measures of protection and promotion under the European Charter for Regional or Minority Languages[2][306] and the Framework Convention for the Protection of National Minorities.[307] In the 2001 Census over a fifth (21%) of the population of Wales said they could speak Welsh,[308] an increase from the 1991 Census (18%).[309] In addition it is estimated that about 200,000 Welsh speakers live in England.[310] In the same census in Northern Ireland 167,487 people (10.4%) stated that they had "some knowledge of Irish" (see Irish language in Northern Ireland), almost exclusively in the nationalist (mainly Catholic) population. Over 92,000 people in Scotland (just under 2% of the population) had some Gaelic language ability, including 72% of those living in the Outer Hebrides.[311] The number of schoolchildren being taught through Welsh, Scottish Gaelic and Irish is increasing.[312] Among emigrant-descended populations some Scottish Gaelic is still spoken in Canada (principally Nova Scotia and Cape Breton Island),[313] and Welsh in Patagonia, Argentina.[314] +Scots, a language descended from early northern Middle English, has limited recognition alongside its regional variant, Ulster Scots in Northern Ireland, without specific commitments to protection and promotion.[2][315] +It is compulsory for pupils to study a second language up to the age of 14 in England,[316] and up to age 16 in Scotland. French and German are the two most commonly taught second languages in England and Scotland. All pupils in Wales are taught Welsh as a second language up to age 16, or are taught in Welsh.[317] +Religion +Main article: Religion in the United Kingdom + +Westminster Abbey is used for the coronation of British monarchs +Forms of Christianity have dominated religious life in what is now the United Kingdom for over 1,400 years.[318] Although a majority of citizens still identify with Christianity in many surveys, regular church attendance has fallen dramatically since the middle of the 20th century,[319] while immigration and demographic change have contributed to the growth of other faiths, most notably Islam.[320] This has led some commentators to variously describe the UK as a multi-faith,[321] secularised,[322] or post-Christian society.[323] +In the 2001 census 71.6% of all respondents indicated that they were Christians, with the next largest faiths (by number of adherents) being Islam (2.8%), Hinduism (1.0%), Sikhism (0.6%), Judaism (0.5%), Buddhism (0.3%) and all other religions (0.3%).[324] 15% of respondents stated that they had no religion, with a further 7% not stating a religious preference.[325] A Tearfund survey in 2007 showed only one in ten Britons actually attend church weekly.[326] Between the 2001 and 2011 census there was a decrease in the amount of people who identified as Christian by 12%, whilst the percentage of those reporting no religious affiliation doubled. This contrasted with growth in the other main religious group categories, with the number of Muslims increasing by the most substantial margin to a total of about 5%.[327] +The Church of England is the established church in England.[328] It retains a representation in the UK Parliament and the British monarch is its Supreme Governor.[329] In Scotland the Presbyterian Church of Scotland is recognised as the national church. It is not subject to state control, and the British monarch is an ordinary member, required to swear an oath to "maintain and preserve the Protestant Religion and Presbyterian Church Government" upon his or her accession.[330][331] The (Anglican) Church in Wales was disestablished in 1920 and, as the (Anglican) Church of Ireland was disestablished in 1870 before the partition of Ireland, there is no established church in Northern Ireland.[332] Although there are no UK-wide data in the 2001 census on adherence to individual Christian denominations, it has been estimated that 62% of Christians are Anglican, 13.5% Catholic, 6% Presbyterian, 3.4% Methodist with small numbers of other Protestant denominations such as Open Brethren, and Orthodox churches.[333] +Migration +Main article: Immigration to the United Kingdom since 1922 +See also: Foreign-born population of the United Kingdom + +Estimated foreign-born population by country of birth, April 2007 – March 2008 +The United Kingdom has experienced successive waves of migration. The Great Famine in Ireland, then part of the United Kingdom, resulted in perhaps a million people migrating to Great Brtain.[334] Unable to return to Poland at the end of World War II, over 120,000 Polish veterans remained in the UK permanently.[335] After World War II, there was significant immigration from the colonies and newly independent former colonies, partly as a legacy of empire and partly driven by labour shortages. Many of these migrants came from the Caribbean and the Indian subcontinent.[336] The British Asian population has increased from 2.2 million in 2001 to over 4.2 million in 2011.[337] +One of the more recent trends in migration has been the arrival of workers from the new EU member states in Eastern Europe. In 2010, there were 7.0 million foreign-born residents in the UK, corresponding to 11.3% of the total population. Of these, 4.76 million (7.7%) were born outside the EU and 2.24 million (3.6%) were born in another EU Member State.[338] The proportion of foreign-born people in the UK remains slightly below that of many other European countries.[339] However, immigration is now contributing to a rising population[340] with arrivals and UK-born children of migrants accounting for about half of the population increase between 1991 and 2001. Analysis of Office for National Statistics (ONS) data shows that a net total of 2.3 million migrants moved to the UK in the 15 years from 1991 to 2006.[341][342] In 2008 it was predicted that migration would add 7 million to the UK population by 2031,[343] though these figures are disputed.[344] The ONS reported that net migration rose from 2009 to 2010 by 21 per cent to 239,000.[345] In 2011 the net increase was 251,000: immigration was 589,000, while the number of people emigrating (for more than 12 months) was 338,000.[346][347] +195,046 foreign nationals became British citizens in 2010,[348] compared to 54,902 in 1999.[348][349] A record 241,192 people were granted permanent settlement rights in 2010, of whom 51 per cent were from Asia and 27 per cent from Africa.[350] 25.5 per cent of babies born in England and Wales in 2011 were born to mothers born outside the UK, according to official statistics released in 2012.[351] +Citizens of the European Union, including those of the UK, have the right to live and work in any EU member state.[352] The UK applied temporary restrictions to citizens of Romania and Bulgaria, which joined the EU in January 2007.[353] Research conducted by the Migration Policy Institute for the Equality and Human Rights Commission suggests that, between May 2004 and September 2009, 1.5 million workers migrated from the new EU member states to the UK, two-thirds of them Polish, but that many subsequently returned home, resulting in a net increase in the number of nationals of the new member states in the UK of some 700,000 over that period.[354][355] The late-2000s recession in the UK reduced the economic incentive for Poles to migrate to the UK,[356] the migration becoming temporary and circular.[357] In 2009, for the first time since enlargement, more nationals of the eight central and eastern European states that had joined the EU in 2004 left the UK than arrived.[358] In 2011, citizens of the new EU member states made up 13% of the immigrants entering the country.[346] + +Estimated number of British citizens living overseas by country, 2006 +The UK government has introduced a points-based immigration system for immigration from outside the European Economic Area to replace former schemes, including the Scottish Government's Fresh Talent Initiative.[359] In June 2010 the UK government introduced a temporary limit of 24,000 on immigration from outside the EU, aiming to discourage applications before a permanent cap was imposed in April 2011.[360] The cap has caused tension within the coalition: business secretary Vince Cable has argued that it is harming British businesses.[361] +Emigration was an important feature of British society in the 19th century. Between 1815 and 1930 around 11.4 million people emigrated from Britain and 7.3 million from Ireland. Estimates show that by the end of the 20th century some 300 million people of British and Irish descent were permanently settled around the globe.[362] Today, at least 5.5 million UK-born people live abroad,[363][364][365] mainly in Australia, Spain, the United States and Canada.[363][366] +Education +Main article: Education in the United Kingdom +See also: Education in England, Education in Northern Ireland, Education in Scotland and Education in Wales + +King's College, part of the University of Cambridge, which was founded in 1209 +Education in the United Kingdom is a devolved matter, with each country having a separate education system. +Whilst education in England is the responsibility of the Secretary of State for Education, the day-to-day administration and funding of state schools is the responsibility of local authorities.[367] Universally free of charge state education was introduced piecemeal between 1870 and 1944.[368][369] Education is now mandatory from ages five to sixteen (15 if born in late July or August). In 2011, the Trends in International Mathematics and Science Study (TIMSS) rated 13–14-year-old pupils in England and Wales 10th in the world for maths and 9th for science.[370] The majority of children are educated in state-sector schools, a small proportion of which select on the grounds of academic ability. Two of the top ten performing schools in terms of GCSE results in 2006 were state-run grammar schools. Over half of students at the leading universities of Cambridge and Oxford had attended state schools.[371] Despite a fall in actual numbers the proportion of children in England attending private schools has risen to over 7%.[372] In 2010, more than 45% of places at the University of Oxford and 40% at the University of Cambridge were taken by students from private schools, even though they educate just 7% of the population.[373] England has the two oldest universities in English-speaking world, Universities of Oxford and Cambridge (jointly known as "Oxbridge") with history of over eight centuries. The United Kingdom has 9 universities featured in the Times Higher Education top 100 rankings, making it second to the United States in terms of representation.[374] + +Queen's University Belfast, built in 1849[375] +Education in Scotland is the responsibility of the Cabinet Secretary for Education and Lifelong Learning, with day-to-day administration and funding of state schools the responsibility of Local Authorities. Two non-departmental public bodies have key roles in Scottish education. The Scottish Qualifications Authority is responsible for the development, accreditation, assessment and certification of qualifications other than degrees which are delivered at secondary schools, post-secondary colleges of further education and other centres.[376] The Learning and Teaching Scotland provides advice, resources and staff development to education professionals.[377] Scotland first legislated for compulsory education in 1496.[378] The proportion of children in Scotland attending private schools is just over 4%, and it has been rising slowly in recent years.[379] Scottish students who attend Scottish universities pay neither tuition fees nor graduate endowment charges, as fees were abolished in 2001 and the graduate endowment scheme was abolished in 2008.[380] +The Welsh Government has responsibility for education in Wales. A significant number of Welsh students are taught either wholly or largely in the Welsh language; lessons in Welsh are compulsory for all until the age of 16.[381] There are plans to increase the provision of Welsh-medium schools as part of the policy of creating a fully bilingual Wales. +Education in Northern Ireland is the responsibility of the Minister of Education and the Minister for Employment and Learning, although responsibility at a local level is administered by five education and library boards covering different geographical areas. The Council for the Curriculum, Examinations & Assessment (CCEA) is the body responsible for advising the government on what should be taught in Northern Ireland's schools, monitoring standards and awarding qualifications.[382] +A government commission's report in 2014 found that privately educated people comprise 7% of the general population of the UK but much larger percentages of the top professions, the most extreme case quoted being 71% of senior judges.[383][384] +Healthcare +Main article: Healthcare in the United Kingdom + +The Royal Aberdeen Children's Hospital, an NHS Scotland specialist children's hospital +Healthcare in the United Kingdom is a devolved matter and each country has its own system of private and publicly funded health care, together with alternative, holistic and complementary treatments. Public healthcare is provided to all UK permanent residents and is mostly free at the point of need, being paid for from general taxation. The World Health Organization, in 2000, ranked the provision of healthcare in the United Kingdom as fifteenth best in Europe and eighteenth in the world.[385][386] +Regulatory bodies are organised on a UK-wide basis such as the General Medical Council, the Nursing and Midwifery Council and non-governmental-based, such as the Royal Colleges. However, political and operational responsibility for healthcare lies with four national executives; healthcare in England is the responsibility of the UK Government; healthcare in Northern Ireland is the responsibility of the Northern Ireland Executive; healthcare in Scotland is the responsibility of the Scottish Government; and healthcare in Wales is the responsibility of the Welsh Assembly Government. Each National Health Service has different policies and priorities, resulting in contrasts.[387][388] +Since 1979 expenditure on healthcare has been increased significantly to bring it closer to the European Union average.[389] The UK spends around 8.4 per cent of its gross domestic product on healthcare, which is 0.5 percentage points below the Organisation for Economic Co-operation and Development average and about one percentage point below the average of the European Union.[390] +Culture +Main article: Culture of the United Kingdom +The culture of the United Kingdom has been influenced by many factors including: the nation's island status; its history as a western liberal democracy and a major power; as well as being a political union of four countries with each preserving elements of distinctive traditions, customs and symbolism. As a result of the British Empire, British influence can be observed in the language, culture and legal systems of many of its former colonies including Australia, Canada, India, Ireland, New Zealand, South Africa and the United States. The substantial cultural influence of the United Kingdom has led it to be described as a "cultural superpower."[391][392] +Literature +Main article: British literature + +The Chandos portrait, believed to depict William Shakespeare +'British literature' refers to literature associated with the United Kingdom, the Isle of Man and the Channel Islands. Most British literature is in the English language. In 2005, some 206,000 books were published in the United Kingdom and in 2006 it was the largest publisher of books in the world.[393] +The English playwright and poet William Shakespeare is widely regarded as the greatest dramatist of all time,[394][395][396] and his contemporaries Christopher Marlowe and Ben Jonson have also been held in continuous high esteem. More recently the playwrights Alan Ayckbourn, Harold Pinter, Michael Frayn, Tom Stoppard and David Edgar have combined elements of surrealism, realism and radicalism. +Notable pre-modern and early-modern English writers include Geoffrey Chaucer (14th century), Thomas Malory (15th century), Sir Thomas More (16th century), John Bunyan (17th century) and John Milton (17th century). In the 18th century Daniel Defoe (author of Robinson Crusoe) and Samuel Richardson were pioneers of the modern novel. In the 19th century there followed further innovation by Jane Austen, the gothic novelist Mary Shelley, the children's writer Lewis Carroll, the Brontë sisters, the social campaigner Charles Dickens, the naturalist Thomas Hardy, the realist George Eliot, the visionary poet William Blake and romantic poet William Wordsworth. 20th-century English writers include the science-fiction novelist H. G. Wells; the writers of children's classics Rudyard Kipling, A. A. Milne (the creator of Winnie-the-Pooh), Roald Dahl and Enid Blyton; the controversial D. H. Lawrence; the modernist Virginia Woolf; the satirist Evelyn Waugh; the prophetic novelist George Orwell; the popular novelists W. Somerset Maugham and Graham Greene; the crime writer Agatha Christie (the best-selling novelist of all time);[397] Ian Fleming (the creator of James Bond); the poets T.S. Eliot, Philip Larkin and Ted Hughes; the fantasy writers J. R. R. Tolkien, C. S. Lewis and J. K. Rowling; the graphic novelist Alan Moore, whose novel Watchmen is often cited by critics as comic's greatest series and graphic novel[398] and one of the best-selling graphic novels ever published.[399] + +A photograph of Victorian era novelist Charles Dickens +Scotland's contributions include the detective writer Arthur Conan Doyle (the creator of Sherlock Holmes), romantic literature by Sir Walter Scott, the children's writer J. M. Barrie, the epic adventures of Robert Louis Stevenson and the celebrated poet Robert Burns. More recently the modernist and nationalist Hugh MacDiarmid and Neil M. Gunn contributed to the Scottish Renaissance. A more grim outlook is found in Ian Rankin's stories and the psychological horror-comedy of Iain Banks. Scotland's capital, Edinburgh, was UNESCO's first worldwide City of Literature.[400] +Britain's oldest known poem, Y Gododdin, was composed in Yr Hen Ogledd (The Old North), most likely in the late 6th century. It was written in Cumbric or Old Welsh and contains the earliest known reference to King Arthur.[401] From around the seventh century, the connection between Wales and the Old North was lost, and the focus of Welsh-language culture shifted to Wales, where Arthurian legend was further developed by Geoffrey of Monmouth.[402] Wales's most celebrated medieval poet, Dafydd ap Gwilym (fl.1320–1370), composed poetry on themes including nature, religion and especially love. He is widely regarded as one of the greatest European poets of his age.[403] Until the late 19th century the majority of Welsh literature was in Welsh and much of the prose was religious in character. Daniel Owen is credited as the first Welsh-language novelist, publishing Rhys Lewis in 1885. The best-known of the Anglo-Welsh poets are both Thomases. Dylan Thomas became famous on both sides of the Atlantic in the mid-20th century. He is remembered for his poetry – his "Do not go gentle into that good night; Rage, rage against the dying of the light." is one of the most quoted couplets of English language verse – and for his 'play for voices', Under Milk Wood. The influential Church in Wales 'poet-priest' and Welsh nationalist R. S. Thomas was nominated for the Nobel Prize in Literature in 1996. Leading Welsh novelists of the twentieth century include Richard Llewellyn and Kate Roberts.[404][405] +Authors of other nationalities, particularly from Commonwealth countries, the Republic of Ireland and the United States, have lived and worked in the UK. Significant examples through the centuries include Jonathan Swift, Oscar Wilde, Bram Stoker, George Bernard Shaw, Joseph Conrad, T.S. Eliot, Ezra Pound and more recently British authors born abroad such as Kazuo Ishiguro and Sir Salman Rushdie.[406][407] +Music +Main article: Music of the United Kingdom +See also: British rock + +The Beatles are the most commercially successful and critically acclaimed band in the history of music, selling over a billion records internationally.[408][409][410] +Various styles of music are popular in the UK from the indigenous folk music of England, Wales, Scotland and Northern Ireland to heavy metal. Notable composers of classical music from the United Kingdom and the countries that preceded it include William Byrd, Henry Purcell, Sir Edward Elgar, Gustav Holst, Sir Arthur Sullivan (most famous for working with the librettist Sir W. S. Gilbert), Ralph Vaughan Williams and Benjamin Britten, pioneer of modern British opera. Sir Peter Maxwell Davies is one of the foremost living composers and current Master of the Queen's Music. The UK is also home to world-renowned symphonic orchestras and choruses such as the BBC Symphony Orchestra and the London Symphony Chorus. Notable conductors include Sir Simon Rattle, John Barbirolli and Sir Malcolm Sargent. Some of the notable film score composers include John Barry, Clint Mansell, Mike Oldfield, John Powell, Craig Armstrong, David Arnold, John Murphy, Monty Norman and Harry Gregson-Williams. George Frideric Handel, although born German, was a naturalised British citizen[411] and some of his best works, such as Messiah, were written in the English language.[412] Andrew Lloyd Webber has achieved enormous worldwide commercial success and is a prolific composer of musical theatre, works which have dominated London's West End for a number of years and have travelled to Broadway in New York.[413] +The Beatles have international sales of over one billion units and are the biggest-selling and most influential band in the history of popular music.[408][409][410][414] Other prominent British contributors to have influenced popular music over the last 50 years include; The Rolling Stones, Led Zeppelin, Pink Floyd, Queen, the Bee Gees, and Elton John, all of whom have world wide record sales of 200 million or more.[415][416][417][418][419][420] The Brit Awards are the BPI's annual music awards, and some of the British recipients of the Outstanding Contribution to Music award include; The Who, David Bowie, Eric Clapton, Rod Stewart and The Police.[421] More recent UK music acts that have had international success include Coldplay, Radiohead, Oasis, Spice Girls, Robbie Williams, Amy Winehouse and Adele.[422] +A number of UK cities are known for their music. Acts from Liverpool have had more UK chart number one hit singles per capita (54) than any other city worldwide.[423] Glasgow's contribution to music was recognised in 2008 when it was named a UNESCO City of Music, one of only three cities in the world to have this honour.[424] +Visual art +Main article: Art of the United Kingdom + +J. M. W. Turner self-portrait, oil on canvas, c. 1799 +The history of British visual art forms part of western art history. Major British artists include: the Romantics William Blake, John Constable, Samuel Palmer and J.M.W. Turner; the portrait painters Sir Joshua Reynolds and Lucian Freud; the landscape artists Thomas Gainsborough and L. S. Lowry; the pioneer of the Arts and Crafts Movement William Morris; the figurative painter Francis Bacon; the Pop artists Peter Blake, Richard Hamilton and David Hockney; the collaborative duo Gilbert and George; the abstract artist Howard Hodgkin; and the sculptors Antony Gormley, Anish Kapoor and Henry Moore. During the late 1980s and 1990s the Saatchi Gallery in London helped to bring to public attention a group of multi-genre artists who would become known as the "Young British Artists": Damien Hirst, Chris Ofili, Rachel Whiteread, Tracey Emin, Mark Wallinger, Steve McQueen, Sam Taylor-Wood and the Chapman Brothers are among the better-known members of this loosely affiliated movement. +The Royal Academy in London is a key organisation for the promotion of the visual arts in the United Kingdom. Major schools of art in the UK include: the six-school University of the Arts London, which includes the Central Saint Martins College of Art and Design and Chelsea College of Art and Design; Goldsmiths, University of London; the Slade School of Fine Art (part of University College London); the Glasgow School of Art; the Royal College of Art; and The Ruskin School of Drawing and Fine Art (part of the University of Oxford). The Courtauld Institute of Art is a leading centre for the teaching of the history of art. Important art galleries in the United Kingdom include the National Gallery, National Portrait Gallery, Tate Britain and Tate Modern (the most-visited modern art gallery in the world, with around 4.7 million visitors per year).[425] +Cinema +Main article: Cinema of the United Kingdom + +Film director Alfred Hitchcock +The United Kingdom has had a considerable influence on the history of the cinema. The British directors Alfred Hitchcock, whose film Vertigo is considered by some critics as the best film of all time,[426] and David Lean are among the most critically acclaimed of all-time.[427] Other important directors including Charlie Chaplin,[428] Michael Powell,[429] Carol Reed[430] and Ridley Scott.[431] Many British actors have achieved international fame and critical success, including: Julie Andrews,[432] Richard Burton,[433] Michael Caine,[434] Charlie Chaplin,[435] Sean Connery,[436] Vivien Leigh,[437] David Niven,[438] Laurence Olivier,[439] Peter Sellers,[440] Kate Winslet,[441] and Daniel Day-Lewis, the only person to win an Oscar in the best actor category three times.[442] Some of the most commercially successful films of all time have been produced in the United Kingdom, including the two highest-grossing film franchises (Harry Potter and James Bond).[443] Ealing Studios has a claim to being the oldest continuously working film studio in the world.[444] +Despite a history of important and successful productions, the industry has often been characterised by a debate about its identity and the level of American and European influence. British producers are active in international co-productions and British actors, directors and crew feature regularly in American films. Many successful Hollywood films have been based on British people, stories or events, including Titanic, The Lord of the Rings, Pirates of the Caribbean. +In 2009, British films grossed around $2 billion worldwide and achieved a market share of around 7% globally and 17% in the United Kingdom.[445] UK box-office takings totalled £944 million in 2009, with around 173 million admissions.[445] The British Film Institute has produced a poll ranking of what it considers to be the 100 greatest British films of all time, the BFI Top 100 British films.[446] The annual British Academy Film Awards, hosted by the British Academy of Film and Television Arts, are the British equivalent of the Oscars.[447] +Media +Main article: Media of the United Kingdom + +Broadcasting House in London, headquarters of the BBC, the oldest and largest broadcaster in the world.[448][449][450] +The BBC, founded in 1922, is the UK's publicly funded radio, television and Internet broadcasting corporation, and is the oldest and largest broadcaster in the world.[448][449][450] It operates numerous television and radio stations in the UK and abroad and its domestic services are funded by the television licence.[451][452] Other major players in the UK media include ITV plc, which operates 11 of the 15 regional television broadcasters that make up the ITV Network,[453] and News Corporation, which owns a number of national newspapers through News International such as the most popular tabloid The Sun and the longest-established daily "broadsheet" The Times,[454] as well as holding a large stake in satellite broadcaster British Sky Broadcasting.[455] London dominates the media sector in the UK: national newspapers and television and radio are largely based there, although Manchester is also a significant national media centre. Edinburgh and Glasgow, and Cardiff, are important centres of newspaper and broadcasting production in Scotland and Wales respectively.[456] The UK publishing sector, including books, directories and databases, journals, magazines and business media, newspapers and news agencies, has a combined turnover of around £20 billion and employs around 167,000 people.[457] +In 2009, it was estimated that individuals viewed a mean of 3.75 hours of television per day and 2.81 hours of radio. In that year the main BBC public service broadcasting channels accounted for an estimated 28.4% of all television viewing; the three main independent channels accounted for 29.5% and the increasingly important other satellite and digital channels for the remaining 42.1%.[458] Sales of newspapers have fallen since the 1970s and in 2009 42% of people reported reading a daily national newspaper.[459] In 2010 82.5% of the UK population were Internet users, the highest proportion amongst the 20 countries with the largest total number of users in that year.[460] +Philosophy +Main article: British philosophy +The United Kingdom is famous for the tradition of 'British Empiricism', a branch of the philosophy of knowledge that states that only knowledge verified by experience is valid, and 'Scottish Philosophy', sometimes referred to as the 'Scottish School of Common Sense'.[461] The most famous philosophers of British Empiricism are John Locke, George Berkeley and David Hume; while Dugald Stewart, Thomas Reid and William Hamilton were major exponents of the Scottish "common sense" school. Two Britons are also notable for a theory of moral philosophy utilitarianism, first used by Jeremy Bentham and later by John Stuart Mill in his short work Utilitarianism.[462][463] Other eminent philosophers from the UK and the unions and countries that preceded it include Duns Scotus, John Lilburne, Mary Wollstonecraft, Sir Francis Bacon, Adam Smith, Thomas Hobbes, William of Ockham, Bertrand Russell and A.J. "Freddie" Ayer. Foreign-born philosophers who settled in the UK include Isaiah Berlin, Karl Marx, Karl Popper and Ludwig Wittgenstein. +Sport +Main article: Sport in the United Kingdom + +Wembley Stadium, London, home of the England national football team, is one of the most expensive stadia ever built.[464] +Major sports, including association football, tennis, rugby union, rugby league, golf, boxing, rowing and cricket, originated or were substantially developed in the UK and the states that preceded it. With the rules and codes of many modern sports invented and codified in late 19th-century Victorian Britain, in 2012, the President of the IOC, Jacques Rogge, stated; "This great, sports-loving country is widely recognized as the birthplace of modern sport. It was here that the concepts of sportsmanship and fair play were first codified into clear rules and regulations. It was here that sport was included as an educational tool in the school curriculum".[465][466] +In most international competitions, separate teams represent England, Scotland and Wales. Northern Ireland and the Republic of Ireland usually field a single team representing all of Ireland, with notable exceptions being association football and the Commonwealth Games. In sporting contexts, the English, Scottish, Welsh and Irish / Northern Irish teams are often referred to collectively as the Home Nations. There are some sports in which a single team represents the whole of United Kingdom, including the Olympics, where the UK is represented by the Great Britain team. The 1908, 1948 and 2012 Summer Olympics were held in London, making it the first city to host the games three times. Britain has participated in every modern Olympic Games to date and is third in the medal count. +A 2003 poll found that football is the most popular sport in the United Kingdom.[467] Each of the Home Nations has its own football association, national team and league system. The English top division, the Premier League, is the most watched football league in the world.[468] The first-ever international football match was contested by England and Scotland on 30 November 1872.[469] England, Scotland, Wales and Northern Ireland compete as separate countries in international competitions.[470] A Great Britain Olympic football team was assembled for the first time to compete in the London 2012 Olympic Games. However, the Scottish, Welsh and Northern Irish football associations declined to participate, fearing that it would undermine their independent status – a fear confirmed by FIFA president Sepp Blatter.[471] + +The Millennium Stadium, Cardiff, opened for the 1999 Rugby World Cup. +Cricket was invented in England. The England cricket team, controlled by the England and Wales Cricket Board,[472] is the only national team in the UK with Test status. Team members are drawn from the main county sides, and include both English and Welsh players. Cricket is distinct from football and rugby where Wales and England field separate national teams, although Wales had fielded its own team in the past. Irish and Scottish players have played for England because neither Scotland nor Ireland have Test status and have only recently started to play in One Day Internationals.[473][474] Scotland, England (and Wales), and Ireland (including Northern Ireland) have competed at the Cricket World Cup, with England reaching the finals on three occasions. There is a professional league championship in which clubs representing 17 English counties and 1 Welsh county compete.[475] +Rugby league is a popular sport in some regions of the UK. It originated in Huddersfield and is generally played in Northern England.[476] A single 'Great Britain Lions' team had competed in the Rugby League World Cup and Test match games, but this changed in 2008 when England, Scotland and Ireland competed as separate nations.[477] Great Britain is still being retained as the full national team for Ashes tours against Australia, New Zealand and France. Super League is the highest level of professional rugby league in the UK and Europe. It consists of 11 teams from Northern England, 1 from London, 1 from Wales and 1 from France. +In rugby union, England, Scotland, Wales, Ireland, France and Italy compete in the Six Nations Championship; the premier international tournament in the northern hemisphere. Sport governing bodies in England, Scotland, Wales and Ireland organise and regulate the game separately.[478] If any of the British teams or the Irish team beat the other three in a tournament, then it is awarded the Triple Crown.[479] + +The Wimbledon Championships, a Grand Slam tennis tournament, is held in Wimbledon, London every June or July. +Thoroughbred racing, which originated under Charles II of England as the "sport of kings", is popular throughout the UK with world-famous races including the Grand National, the Epsom Derby, Royal Ascot and the Cheltenham National Hunt Festival (including the Cheltenham Gold Cup). The UK has proved successful in the international sporting arena in rowing. +The UK is closely associated with motorsport. Many teams and drivers in Formula One (F1) are based in the UK, and the country has won more drivers' and constructors' titles than any other. The UK hosted the very first F1 Grand Prix in 1950 at Silverstone, the current location of the British Grand Prix held each year in July. The country also hosts legs of the Grand Prix motorcycle racing, World Rally Championship and FIA World Endurance Championship. The premier national auto racing event is the British Touring Car Championship (BTCC). Motorcycle road racing has a long tradition with races such as the Isle of Man TT and the North West 200. +Golf is the sixth-most popular sport, by participation, in the UK. Although The Royal and Ancient Golf Club of St Andrews in Scotland is the sport's home course,[480] the world's oldest golf course is actually Musselburgh Links' Old Golf Course.[481] +Snooker is one of the UK's popular sporting exports, with the world championships held annually in Sheffield.[482] The modern game of lawn tennis first originated in the city of Birmingham between 1859 and 1865.[483] The Championships, Wimbledon are international tennis events held in Wimbledon in south London every summer and are regarded as the most prestigious event of the global tennis calendar. In Northern Ireland Gaelic football and hurling are popular team sports, both in terms of participation and spectating, and Irish expatriates in the UK and the US also play them.[484] Shinty (or camanachd) is popular in the Scottish Highlands.[485] +Symbols +Main article: Symbols of the United Kingdom, the Channel Islands and the Isle of Man + +The Statue of Britannia in Plymouth. Britannia is a national personification of the UK. +The flag of the United Kingdom is the Union Flag (also referred to as the Union Jack). It was created in 1606 by the superimposition of the Flag of England on the Flag of Scotland and updated in 1801 with the addition of Saint Patrick's Flag. Wales is not represented in the Union Flag, as Wales had been conquered and annexed to England prior to the formation of the United Kingdom. The possibility of redesigning the Union Flag to include representation of Wales has not been completely ruled out.[486] The national anthem of the United Kingdom is "God Save the King", with "King" replaced with "Queen" in the lyrics whenever the monarch is a woman. +Britannia is a national personification of the United Kingdom, originating from Roman Britain.[487] Britannia is symbolised as a young woman with brown or golden hair, wearing a Corinthian helmet and white robes. She holds Poseidon's three-pronged trident and a shield, bearing the Union Flag. Sometimes she is depicted as riding on the back of a lion. Since the height of the British Empire in the late 19th century, Britannia has often been associated with British maritime dominance, as in the patriotic song "Rule, Britannia!". Up until 2008, the lion symbol was depicted behind Britannia on the British fifty pence coin and on the back of the British ten pence coin. It is also used as a symbol on the non-ceremonial flag of the British Army. The bulldog is sometimes used as a symbol of the United Kingdom and has been associated with Winston Churchill's defiance of Nazi Germany.[488] +See also +Outline of the United Kingdom + United Kingdom – Wikipedia book +Walking in the United Kingdom +Flag of the United Kingdom.svgUnited Kingdom portal Flag of Europe.svgEuropean Union portal Europe green light.pngEurope portal +Notes +Jump up ^ The Royal coat of arms used in Scotland: + Royal Coat of Arms of the United Kingdom (Scotland).svg +Jump up ^ There is no authorised version of the national anthem as the words are a matter of tradition; only the first verse is usually sung.[1] No law was passed making "God Save the Queen" the official anthem. In the English tradition, such laws are not necessary; proclamation and usage are sufficient to make it the national anthem. "God Save the Queen" also serves as the Royal anthem for several other countries, namely certain Commonwealth realms. +Jump up ^ Under the Council of Europe's European Charter for Regional or Minority Languages, Scots, Ulster-Scots, Welsh, Cornish, Irish and Scottish Gaelic, are officially recognised as regional or minority languages by the British government for the purposes of the Charter. See also Languages of the United Kingdom.[2] +Jump up ^ Although Northern Ireland is the only part of the UK that shares a land border with another state, two of its Overseas Territories also share land borders with other states. Gibraltar shares a border with Spain, while the Sovereign Base Areas of Akrotiri and Dhekelia share borders with the Republic of Cyprus, Turkish Republic of Northern Cyprus and UN buffer zone separating the two Cypriot polities. +Jump up ^ The Anglo-Irish Treaty was signed on 6 December 1921 to resolve the Irish War of Independence. Effective one year later, it established the Irish Free State as a separate dominion within the Commonwealth. The UK's current name was adopted in 1927 to reflect the change. +Jump up ^ Compare to section 1 of both of the 1800 Acts of Union which reads: the Kingdoms of Great Britain and Ireland shall...be united into one Kingdom, by the Name of "The United Kingdom of Great Britain and Ireland" +Jump up ^ New Zealand, Israel and San Marino are the other countries with uncodified constitutions. +Jump up ^ Since the early twentieth century the prime minister has held the office of First Lord of the Treasury, and in recent decades has also held the office of Minister for the Civil Service. +Jump up ^ Sinn Féin, an Irish republican party, also contests elections in the Republic of Ireland. +Jump up ^ In 2007–2008, this was calculated to be £115 per week for single adults with no dependent children; £199 per week for couples with no dependent children; £195 per week for single adults with two dependent children under 14; and £279 per week for couples with two dependent children under 14. +References +Jump up ^ National Anthem, British Monarchy official website. Retrieved 16 November 2013. +^ Jump up to: a b c "List of declarations made with respect to treaty No. 148". Council of Europe. Retrieved 12 December 2013. +^ Jump up to: a b "Population Estimates for UK, England and Wales, Scotland and Northern Ireland, Mid-2013". Office for National Statistics. Retrieved 26 June 2014. +Jump up ^ "2011 UK censuses". Office for National Statistics. Retrieved 17 December 2012. +^ Jump up to: a b c d "United Kingdom". International Monetary Fund. Retrieved 1 November 2014. +Jump up ^ "Gini coefficient of equivalised disposable income (source: SILC)". Eurostat Data Explorer. Retrieved 13 August 2013. +Jump up ^ "2014 Human Development Report". 14 March 2013. pp. 22–25. Retrieved 27 July 2014. +Jump up ^ "Definition of Great Britain in English". Oxford University Press. Retrieved 29 October 2014. Great Britain is the name for the island that comprises England, Scotland, and Wales, although the term is also used loosely to refer to the United Kingdom. +Jump up ^ The British Monarchy, What is constitutional monarchy?. Retrieved 17 July 2013 +Jump up ^ CIA, The World Factbook. Retrieved 17 July 2013 +Jump up ^ "The World Factbook". Central Intelligence Agency. 1 February 2014. Retrieved 23 February 2014. +^ Jump up to: a b "Countries within a country". Prime Minister's Office. 10 January 2003. +^ Jump up to: a b "Devolution of powers to Scotland, Wales, and Northern Ireland". United Kingdom Government. Retrieved 17 April 2013. In a similar way to how the government is formed from members from the two Houses of Parliament, members of the devolved legislatures nominate ministers from among themselves to comprise an executive, known as the devolved administrations... +Jump up ^ "Fall in UK university students". BBC News. 29 January 2009. +Jump up ^ "Country Overviews: United Kingdom". Transport Research Knowledge Centre. Retrieved 28 March 2010. +Jump up ^ "Key facts about the United Kingdom". Directgov. Retrieved 3 May 2011. The full title of this country is 'the United Kingdom of Great Britain and Northern Ireland'. 'The UK' is made up of England, Scotland, Wales and Northern Ireland. 'Britain' is used informally, usually meaning the United Kingdom. 'Great Britain' is made up of England, Scotland and Wales. The Channel Islands and the Isle of Man are not part of the UK.[dead link] +Jump up ^ "Working with Overseas Territories". Foreign and Commonwealth Office. Retrieved 3 May 2011. +Jump up ^ Mathias, P. (2001). The First Industrial Nation: the Economic History of Britain, 1700–1914. London: Routledge. ISBN 0-415-26672-6. +Jump up ^ Ferguson, Niall (2004). Empire: The rise and demise of the British world order and the lessons for global power. New York: Basic Books. ISBN 0-465-02328-2. +Jump up ^ Sheridan, Greg (15 May 2010). "Cameron has chance to make UK great again". The Australian (Sydney). Retrieved 23 May 2011. +Jump up ^ Dugan, Emily (18 November 2012). "Britain is now most powerful nation on earth". The Independent (London). Retrieved 18 November 2012. +^ Jump up to: a b "The 15 countries with the highest military expenditure in 2013 (table)" (PDF). Stockholm International Peace Research Institute. Retrieved 4 May 2014. +^ Jump up to: a b The Military Balance 2014: Top 15 Defence Budgets 2013 (IISS) +Jump up ^ "Treaty of Union, 1706". Scots History Online. Retrieved 23 August 2011. +Jump up ^ Barnett, Hilaire; Jago, Robert (2011). Constitutional & Administrative Law (8th ed.). Abingdon: Routledge. p. 165. ISBN 978-0-415-56301-7. +Jump up ^ Gascoigne, Bamber. "History of Great Britain (from 1707)". History World. Retrieved 18 July 2011. +Jump up ^ Cottrell, P. (2008). The Irish Civil War 1922–23. p. 85. ISBN 1-84603-270-9. +^ Jump up to: a b S. Dunn; H. Dawson (2000), An Alphabetical Listing of Word, Name and Place in Northern Ireland and the Living Language of Conflict, Lampeter: Edwin Mellen Press, One specific problem - in both general and particular senses - is to know what to call Northern Ireland itself: in the general sense, it is not a country, or a province, or a state - although some refer to it contemptuously as a statelet: the least controversial word appears to be jurisdiction, but this might change. +Jump up ^ "Changes in the list of subdivision names and code elements". ISO 3166-2. International Organization for Standardization. 15 December 2011. Retrieved 28 May 2012. +Jump up ^ Population Trends, Issues 75–82, p.38, 1994, UK Office of Population Censuses and Surveys +Jump up ^ Life in the United Kingdom: a journey to citizenship, p. 7, United Kingdom Home Office, 2007, ISBN 978-0-11-341313-3. +Jump up ^ "Statistical bulletin: Regional Labour Market Statistics". Retrieved 5 March 2014. +Jump up ^ "13.4% Fall In Earnings Value During Recession". Retrieved 5 March 2014. +Jump up ^ Murphy, Dervla (1979). A Place Apart. London: Penguin. ISBN 978-0-14-005030-1. +Jump up ^ Whyte, John; FitzGerald, Garret (1991). Interpreting Northern Ireland. Oxford: Clarendon Press. ISBN 978-0-19-827380-6. +Jump up ^ "Guardian Unlimited Style Guide". London: Guardian News and Media Limited. 19 December 2008. Retrieved 23 August 2011. +Jump up ^ "BBC style guide (Great Britain)". BBC News. 19 August 2002. Retrieved 23 August 2011. +Jump up ^ "Key facts about the United Kingdom". Government, citizens and rights. HM Government. Retrieved 24 August 2011.[dead link] +Jump up ^ "Merriam-Webster Dictionary Online Definition of ''Great Britain''". Merriam Webster. 31 August 2012. Retrieved 9 April 2013. +Jump up ^ New Oxford American Dictionary: "Great Britain: England, Wales, and Scotland considered as a unit. The name is also often used loosely to refer to the United Kingdom." +Jump up ^ "Great Britain". International Olympic Committee. Retrieved 10 May 2011. +Jump up ^ "Team GB – Our Greatest Team". British Olympic Association. Retrieved 10 May 2011.[dead link] +Jump up ^ Bradley, Anthony Wilfred; Ewing, Keith D. (2007). Constitutional and administrative law 1 (14th ed.). Harlow: Pearson Longman. p. 36. ISBN 978-1-4058-1207-8. +Jump up ^ "Which of these best describes the way you think of yourself?". Northern Ireland Life and Times Survey 2010. ARK – Access Research Knowledge. 2010. Retrieved 1 July 2010. +Jump up ^ Schrijver, Frans (2006). Regionalism after regionalisation: Spain, France and the United Kingdom. Amsterdam University Press. pp. 275–277. ISBN 978-90-5629-428-1. +Jump up ^ Jack, Ian (11 December 2010). "Why I'm saddened by Scotland going Gaelic". The Guardian (London). +Jump up ^ Ffeithiau allweddol am y Deyrnas Unedig : Directgov – Llywodraeth, dinasyddion a hawliau[dead link] +Jump up ^ "Ancient skeleton was 'even older'". BBC News. 30 October 2007. Retrieved 27 April 2011. +Jump up ^ Koch, John T. (2006). Celtic culture: A historical encyclopedia. Santa Barbara, CA: ABC-CLIO. p. 973. ISBN 978-1-85109-440-0. +Jump up ^ Davies, John; Jenkins, Nigel; Baines, Menna; Lynch, Peredur I., eds. (2008). The Welsh Academy Encyclopaedia of Wales. Cardiff: University of Wales Press. p. 915. ISBN 978-0-7083-1953-6. +Jump up ^ "Short Athelstan biography". BBC History. Retrieved 9 April 2013. +Jump up ^ Mackie, J.D. (1991). A History of Scotland. London: Penguin. pp. 18–19. ISBN 978-0-14-013649-4. +Jump up ^ Campbell, Ewan (1999). Saints and Sea-kings: The First Kingdom of the Scots. Edinburgh: Canongate. pp. 8–15. ISBN 0-86241-874-7. +Jump up ^ Haigh, Christopher (1990). The Cambridge Historical Encyclopedia of Great Britain and Ireland. Cambridge University Press. p. 30. ISBN 978-0-521-39552-6. +Jump up ^ Ganshof, F.L. (1996). Feudalism. University of Toronto. p. 165. ISBN 978-0-8020-7158-3. +Jump up ^ Chibnall, Marjorie (1999). The debate on the Norman Conquest. Manchester University Press. pp. 115–122. ISBN 978-0-7190-4913-2. +Jump up ^ Keen, Maurice. "The Hundred Years War". BBC History. +Jump up ^ The Reformation in England and Scotland and Ireland: The Reformation Period & Ireland under Elizabth I, Encyclopædia Britannica Online. +Jump up ^ "British History in Depth – Wales under the Tudors". BBC History. 5 November 2009. Retrieved 21 September 2010. +Jump up ^ Nicholls, Mark (1999). A history of the modern British Isles, 1529–1603: The two kingdoms. Oxford: Blackwell. pp. 171–172. ISBN 978-0-631-19334-0. +Jump up ^ Canny, Nicholas P. (2003). Making Ireland British, 1580–1650. Oxford University Press. pp. 189–200. ISBN 978-0-19-925905-2. +Jump up ^ Ross, D. (2002). Chronology of Scottish History. Glasgow: Geddes & Grosset. p. 56. ISBN 1-85534-380-0 +Jump up ^ Hearn, J. (2002). Claiming Scotland: National Identity and Liberal Culture. Edinburgh University Press. p. 104. ISBN 1-902930-16-9 +Jump up ^ "English Civil Wars". Encyclopaedia Britannica. Retrieved 28 April 2013. +Jump up ^ "Scotland and the Commonwealth: 1651–1660". Archontology.org. 14 March 2010. Retrieved 20 April 2010. +Jump up ^ Lodge, Richard (2007) [1910]. The History of England – From the Restoration to the Death of William III (1660–1702). Read Books. p. 8. ISBN 978-1-4067-0897-4. +Jump up ^ "Tudor Period and the Birth of a Regular Navy". Royal Navy History. Institute of Naval History. Retrieved 24 December 2010.[dead link] +Jump up ^ Canny, Nicholas (1998). The Origins of Empire, The Oxford History of the British Empire Volume I. Oxford University Press. ISBN 0-19-924676-9. +Jump up ^ "Articles of Union with Scotland 1707". UK Parliament. Retrieved 19 October 2008. +Jump up ^ "Acts of Union 1707". UK Parliament. Retrieved 6 January 2011. +Jump up ^ "Treaty (act) of Union 1706". Scottish History online. Retrieved 3 February 2011. +Jump up ^ Library of Congress, The Impact of the American Revolution Abroad, p. 73. +Jump up ^ Loosemore, Jo (2007). Sailing against slavery. BBC Devon. 2007. +Jump up ^ "The Act of Union". Act of Union Virtual Library. Retrieved 15 May 2006. +Jump up ^ Tellier, L.-N. (2009). Urban World History: an Economic and Geographical Perspective. Quebec: PUQ. p. 463. ISBN 2-7605-1588-5. +Jump up ^ Sondhaus, L. (2004). Navies in Modern World History. London: Reaktion Books. p. 9. ISBN 1-86189-202-0. +Jump up ^ Porter, Andrew (1998). The Nineteenth Century, The Oxford History of the British Empire Volume III. Oxford University Press. p. 332. ISBN 0-19-924678-5. +Jump up ^ "The Workshop of the World". BBC History. Retrieved 28 April 2013. +Jump up ^ Porter, Andrew (1998). The Nineteenth Century, The Oxford History of the British Empire Volume III. Oxford University Press. p. 8. ISBN 0-19-924678-5. +Jump up ^ Marshall, P.J. (1996). The Cambridge Illustrated History of the British Empire. Cambridge University Press. pp. 156–57. ISBN 0-521-00254-0. +Jump up ^ Tompson, Richard S. (2003). Great Britain: a reference guide from the Renaissance to the present. New York: Facts on File. p. 63. ISBN 978-0-8160-4474-0. +Jump up ^ Hosch, William L. (2009). World War I: People, Politics, and Power. America at War. New York: Britannica Educational Publishing. p. 21. ISBN 978-1-61530-048-8. +Jump up ^ Turner, John (1988). Britain and the First World War. London: Unwin Hyman. pp. 22–35. ISBN 978-0-04-445109-9. +^ Jump up to: a b Westwell, I.; Cove, D. (eds) (2002). History of World War I, Volume 3. London: Marshall Cavendish. pp. 698 and 705. ISBN 0-7614-7231-2. +Jump up ^ Turner, J. (1988). Britain and the First World War. Abingdon: Routledge. p. 41. ISBN 0-04-445109-1. +Jump up ^ SR&O 1921, No. 533 of 3 May 1921. +Jump up ^ "The Anglo-Irish Treaty, 6 December 1921". CAIN. Retrieved 15 May 2006. +Jump up ^ Rubinstein, W. D. (2004). Capitalism, Culture, and Decline in Britain, 1750–1990. Abingdon: Routledge. p. 11. ISBN 0-415-03719-0. +Jump up ^ "Britain to make its final payment on World War II loan from U.S.". The New York Times. 28 December 2006. Retrieved 25 August 2011. +Jump up ^ Francis, Martin (1997). Ideas and policies under Labour, 1945–1951: Building a new Britain. Manchester University Press. pp. 225–233. ISBN 978-0-7190-4833-3. +Jump up ^ Lee, Stephen J. (1996). Aspects of British political history, 1914–1995. London; New York: Routledge. pp. 173–199. ISBN 978-0-415-13103-2. +Jump up ^ Larres, Klaus (2009). A companion to Europe since 1945. Chichester: Wiley-Blackwell. p. 118. ISBN 978-1-4051-0612-2. +Jump up ^ "Country List". Commonwealth Secretariat. 19 March 2009. Retrieved 11 September 2012.[dead link] +Jump up ^ Julios, Christina (2008). Contemporary British identity: English language, migrants, and public discourse. Studies in migration and diaspora. Aldershot: Ashgate. p. 84. ISBN 978-0-7546-7158-9. +Jump up ^ Aughey, Arthur (2005). The Politics of Northern Ireland: Beyond the Belfast Agreement. London: Routledge. p. 7. ISBN 978-0-415-32788-6. +Jump up ^ "The troubles were over, but the killing continued. Some of the heirs to Ireland's violent traditions refused to give up their inheritance." Holland, Jack (1999). Hope against History: The Course of Conflict in Northern Ireland. New York: Henry Holt. p. 221. ISBN 978-0-8050-6087-4. +Jump up ^ Elliot, Marianne (2007). The Long Road to Peace in Northern Ireland: Peace Lectures from the Institute of Irish Studies at Liverpool University. University of Liverpool Institute of Irish Studies, Liverpool University Press. p. 2. ISBN 1-84631-065-2. +Jump up ^ Dorey, Peter (1995). British politics since 1945. Making contemporary Britain. Oxford: Blackwell. pp. 164–223. ISBN 978-0-631-19075-2. +Jump up ^ Griffiths, Alan; Wall, Stuart (2007). Applied Economics (11th ed.). Harlow: Financial Times Press. p. 6. ISBN 978-0-273-70822-3. Retrieved 26 December 2010. +Jump up ^ Keating, Michael (1 January 1998). "Reforging the Union: Devolution and Constitutional Change in the United Kingdom". Publius: the Journal of Federalism 28 (1): 217. doi:10.1093/oxfordjournals.pubjof.a029948. Retrieved 4 February 2009. +Jump up ^ Jackson, Mike (3 April 2011). "Military action alone will not save Libya". Financial Times (London). +Jump up ^ "United Kingdom country profile". BBC. 24 January 2013. Retrieved 9 April 2013. +Jump up ^ "Scotland to hold independence poll in 2014 – Salmond". BBC News. 10 January 2012. Retrieved 10 January 2012. +Jump up ^ Oxford English Dictionary: "British Isles: a geographical term for the islands comprising Great Britain and Ireland with all their offshore islands including the Isle of Man and the Channel Islands." +^ Jump up to: a b c d e f "United Kingdom". The World Factbook. Central Intelligence Agency. Retrieved 23 September 2008. +^ Jump up to: a b c d e Latimer Clarke Corporation Pty Ltd. "United Kingdom – Atlapedia Online". Atlapedia.com. Retrieved 26 October 2010. +Jump up ^ ROG Learing Team (23 August 2002). "The Prime Meridian at Greenwich". Royal Museums Greenwich. Royal Museums Greenwich. Retrieved 11 September 2012. +Jump up ^ Neal, Clare. "How long is the UK coastline?". British Cartographic Society. Retrieved 26 October 2010. +Jump up ^ "The Channel Tunnel". Eurotunnel. Retrieved 29 November 2010.[dead link] +Jump up ^ "England – Profile". BBC News. 11 February 2010. +Jump up ^ "Scotland Facts". Scotland Online Gateway. Archived from the original on 21 June 2008. Retrieved 16 July 2008. +Jump up ^ Winter, Jon (19 May 2001). "The complete guide to Scottish Islands". The Independent (London). +Jump up ^ "Overview of Highland Boundary Fault". Gazetteer for Scotland. University of Edinburgh. Retrieved 27 December 2010. +Jump up ^ "Ben Nevis Weather". Ben Nevis Weather. Retrieved 26 October 2008. +Jump up ^ "Profile: Wales". BBC News. 9 June 2010. Retrieved 7 November 2010. +Jump up ^ Giles Darkes (26 April 2014). "How long is the UK coastline?". The British Cartographic Society. +Jump up ^ "Geography of Northern Ireland". University of Ulster. Retrieved 22 May 2006. +Jump up ^ "UK climate summaries". Met Office. Retrieved 1 May 2011. +Jump up ^ United Nations Economic and Social Council (August 2007). "Ninth UN Conference on the standardization of Geographical Names". UN Statistics Division. Archived from the original on 1 December 2009. Retrieved 21 October 2008. +Jump up ^ Barlow, I.M. (1991). Metropolitan Government. London: Routledge. ISBN 978-0-415-02099-2. +Jump up ^ "Welcome to the national site of the Government Office Network". Government Offices. Archived from the original on 15 June 2009. Retrieved 3 July 2008. +Jump up ^ "A short history of London government". Greater London Authority. Archived from the original on 21 April 2008. Retrieved 4 October 2008. +Jump up ^ Sherman, Jill; Norfolk, Andrew (5 November 2004). "Prescott's dream in tatters as North East rejects assembly". The Times (London). Retrieved 15 February 2008. The Government is now expected to tear up its twelve-year-old plan to create eight or nine regional assemblies in England to mirror devolution in Scotland and Wales. (subscription required) +Jump up ^ "Local Authority Elections". Local Government Association. Retrieved 3 October 2008.[dead link] +Jump up ^ "STV in Scotland: Local Government Elections 2007". Political Studies Association. Archived from the original on 20 March 2011. Retrieved 2 August 2008. +Jump up ^ Ethical Standards in Public Life framework: "Ethical Standards in Public Life". The Scottish Government. Retrieved 3 October 2008. +Jump up ^ "Who we are". Convention of Scottish Local Authorities. Retrieved 5 July 2011. +Jump up ^ "Local Authorities". The Welsh Assembly Government. Retrieved 31 July 2008. +Jump up ^ "Local government elections in Wales". The Electoral Commission. 2008. Retrieved 8 April 2011. +Jump up ^ "Welsh Local Government Association". Welsh Local Government Association. Retrieved 20 March 2008. +Jump up ^ Devenport, Mark (18 November 2005). "NI local government set for shake-up". BBC News. Retrieved 15 November 2008. +Jump up ^ "Foster announces the future shape of local government" (Press release). Northern Ireland Executive. 13 March 2008. Retrieved 20 October 2008. +Jump up ^ "Local Government elections to be aligned with review of public administration" (Press release). Northern Ireland Office. 25 April 2008. Retrieved 2 August 2008.[dead link] +Jump up ^ "CIBC PWM Global – Introduction to The Cayman Islands". Cibc.com. 11 July 2012. Retrieved 17 August 2012. +Jump up ^ Rappeport, Laurie. "Cayman Islands Tourism". Washington DC: USA Today Travel Tips. Retrieved 9 April 2013. +Jump up ^ "Working with Overseas Territories". Foreign & Commonwealth Office. 6 October 2010. Retrieved 5 November 2010. +Jump up ^ http://www.justice.gov.uk/downloads/about/moj/our-responsibilities/Background_Briefing_on_the_Crown_Dependencies2.pdf +Jump up ^ "Overseas Territories". Foreign & Commonwealth Office. Retrieved 6 September 2010. +Jump up ^ "The World Factbook". CIA. Retrieved 26 December 2010. +Jump up ^ "Country profiles". Foreign & Commonwealth Office. 21 February 2008. Retrieved 6 September 2010.[dead link] +Jump up ^ Davison, Phil (18 August 1995). "Bermudians vote to stay British". The Independent (London). Retrieved 11 September 2012. +Jump up ^ The Committee Office, House of Commons. "House of Commons – Crown Dependencies – Justice Committee". Publications.parliament.uk. Retrieved 7 November 2010. +Jump up ^ Fact sheet on the UK's relationship with the Crown Dependencies – gov.uk, Ministry of Justice. Retrieved 25 August 2014. +Jump up ^ "Profile of Jersey". States of Jersey. Retrieved 31 July 2008. The legislature passes primary legislation, which requires approval by The Queen in Council, and enacts subordinate legislation in many areas without any requirement for Royal Sanction and under powers conferred by primary legislation. +Jump up ^ "Chief Minister to meet Channel Islands counterparts – Isle of Man Public Services" (Press release). Isle of Man Government. 29 May 2012. Retrieved 9 April 2013.[dead link] +Jump up ^ Bagehot, Walter (1867). The English Constitution. London: Chapman and Hall. p. 103. +Jump up ^ Carter, Sarah. "A Guide To the UK Legal System". University of Kent at Canterbury. Retrieved 16 May 2006. +Jump up ^ "Parliamentary sovereignty". UK Parliament. n.d. Archived from the original on 27 May 2012. +Jump up ^ "The Government, Prime Minister and Cabinet". Public services all in one place. Directgov. Retrieved 12 February 2010. +Jump up ^ "Brown is UK's new prime minister". BBC News. 27 June 2007. Retrieved 23 January 2008. +Jump up ^ "David Cameron is UK's new prime minister". BBC News. 11 May 2010. Retrieved 11 May 2010. +Jump up ^ November 2010 "Elections and voting". UK Parliament. Archived from the original on 14 November 2010. Retrieved 14 November 2010. +Jump up ^ November 2010 "The Parliament Acts". UK Parliament. Archived from the original on 14 November 2010. +Jump up ^ "United Kingdom". European Election Database. Norwegian Social Science Data Services. Retrieved 3 July 2010. +Jump up ^ Wainwright, Martin (28 May 2010). "Thirsk and Malton: Conservatives take final seat in parliament". The Guardian (London). Retrieved 3 July 2010. +Jump up ^ "Scots MPs attacked over fees vote". BBC News. 27 January 2004. Retrieved 21 October 2008. +Jump up ^ Taylor, Brian (1 June 1998). "Talking Politics: The West Lothian Question". BBC News. Retrieved 21 October 2008. +Jump up ^ "England-only laws 'need majority from English MPs'". BBC News. 25 March 2013. Retrieved 28 April 2013. +Jump up ^ "Scotland's Parliament – powers and structures". BBC News. 8 April 1999. Retrieved 21 October 2008. +Jump up ^ "Salmond elected as first minister". BBC News. 16 May 2007. Retrieved 21 October 2008. +Jump up ^ "Scottish election: SNP wins election". BBC News. 6 May 2011. +Jump up ^ "Structure and powers of the Assembly". BBC News. 9 April 1999. Retrieved 21 October 2008. +Jump up ^ "Carwyn Jones clinches leadership in Wales". WalesOnline (Media Wales). 1 December 2009. Retrieved 1 December 2009. +Jump up ^ "Devolved Government – Ministers and their departments". Northern Ireland Executive. Archived from the original on 22 August 2007. +Jump up ^ Burrows, N. (1999). "Unfinished Business: The Scotland Act 1998". The Modern Law Review 62 (2): 241–60 [p. 249]. doi:10.1111/1468-2230.00203. The UK Parliament is sovereign and the Scottish Parliament is subordinate. The White Paper had indicated that this was to be the approach taken in the legislation. The Scottish Parliament is not to be seen as a reflection of the settled will of the people of Scotland or of popular sovereignty but as a reflection of its subordination to a higher legal authority. Following the logic of this argument, the power of the Scottish Parliament to legislate can be withdrawn or overridden... +Jump up ^ Elliot, M. (2004). "United Kingdom: Parliamentary sovereignty under pressure". International Journal of Constitutional Law 2 (3): 545–627 [pp. 553–554]. doi:10.1093/icon/2.3.545. Notwithstanding substantial differences among the schemes, an important common factor is that the U.K. Parliament has not renounced legislative sovereignty in relation to the three nations concerned. For example, the Scottish Parliament is empowered to enact primary legislation on all matters, save those in relation to which competence is explicitly denied ... but this power to legislate on what may be termed "devolved matters" is concurrent with the Westminster Parliament's general power to legislate for Scotland on any matter at all, including devolved matters ... In theory, therefore, Westminster may legislate on Scottish devolved matters whenever it chooses... +Jump up ^ Walker, G. (2010). "Scotland, Northern Ireland, and Devolution, 1945–1979". Journal of British Studies 39 (1): 124 & 133. doi:10.1086/644536. +Jump up ^ Gamble, A. "The Constitutional Revolution in the United Kingdom". Publius 36 (1): 19–35 [p. 29]. doi:10.1093/publius/pjj011. The British parliament has the power to abolish the Scottish parliament and the Welsh assembly by a simple majority vote in both houses, but since both were sanctioned by referenda, it would be politically difficult to abolish them without the sanction of a further vote by the people. In this way several of the constitutional measures introduced by the Blair government appear to be entrenched and not subject to a simple exercise of parliamentary sovereignty at Westminster. +Jump up ^ Meehan, E. (1999). "The Belfast Agreement—Its Distinctiveness and Points of Cross-Fertilization in the UK's Devolution Programme". Parliamentary Affairs 52 (1): 19–31 [p. 23]. doi:10.1093/pa/52.1.19. [T]he distinctive involvement of two governments in the Northern Irish problem means that Northern Ireland's new arrangements rest upon an intergovernmental agreement. If this can be equated with a treaty, it could be argued that the forthcoming distribution of power between Westminster and Belfast has similarities with divisions specified in the written constitutions of federal states... Although the Agreement makes the general proviso that Westminster's 'powers to make legislation for Northern Ireland' remains 'unaffected', without an explicit categorical reference to reserved matters, it may be more difficult than in Scotland or Wales for devolved powers to be repatriated. The retraction of devolved powers would not merely entail consultation in Northern Ireland backed implicitly by the absolute power of parliamentary sovereignty but also the renegotiation of an intergovernmental agreement. +Jump up ^ "The Treaty (act) of the Union of Parliament 1706". Scottish History Online. Retrieved 5 October 2008. +Jump up ^ "UK Supreme Court judges sworn in". BBC News. 1 October 2009. +Jump up ^ "Constitutional reform: A Supreme Court for the United Kingdom". Department for Constitutional Affairs. July 2003. Retrieved 13 May 2013. +Jump up ^ "Role of the JCPC". Judicial Committee of the Privy Council. Retrieved 28 April 2013. +Jump up ^ Bainham, Andrew (1998). The international survey of family law: 1996. The Hague: Martinus Nijhoff. p. 298. ISBN 978-90-411-0573-8. +Jump up ^ Adeleye, Gabriel; Acquah-Dadzie, Kofi; Sienkewicz, Thomas; McDonough, James (1999). World dictionary of foreign expressions. Waucojnda, IL: Bolchazy-Carducci. p. 371. ISBN 978-0-86516-423-9. +Jump up ^ "The Australian courts and comparative law". Australian Law Postgraduate Network. Retrieved 28 December 2010. +Jump up ^ "Court of Session – Introduction". Scottish Courts. Retrieved 5 October 2008.[dead link] +Jump up ^ "High Court of Justiciary – Introduction". Scottish Courts. Retrieved 5 October 2008.[dead link] +Jump up ^ "House of Lords – Practice Directions on Permission to Appeal". UK Parliament. Retrieved 22 June 2009. +Jump up ^ "Introduction". Scottish Courts. Retrieved 5 October 2008.[dead link] +Jump up ^ Samuel Bray (2005). "Not proven: introducing a third verdict". The University of Chicago Law Review 72 (4): 1299. Retrieved 30 November 2013. +Jump up ^ "Police-recorded crime down by 9%". BBC News. 17 July 2008. Retrieved 21 October 2008. +Jump up ^ "New record high prison population". BBC News. 8 February 2008. Retrieved 21 October 2008. +Jump up ^ "Crime falls to 32 year low" (Press release). Scottish Government. 7 September 2010. Retrieved 21 April 2011. +Jump up ^ "Prisoner Population at Friday 22 August 2008". Scottish Prison Service. Retrieved 28 August 2008. +Jump up ^ "Scots jail numbers at record high". BBC News. 29 August 2008. Retrieved 21 October 2008. +Jump up ^ Swaine, Jon (13 January 2009). "Barack Obama presidency will strengthen special relationship, says Gordon Brown". The Daily Telegraph (London). Retrieved 3 May 2011. +Jump up ^ Kirchner, E. J.; Sperling, J. (2007). Global Security Governance: Competing Perceptions of Security in the 21st Century. London: Taylor & Francis. p. 100. ISBN 0-415-39162-8 +Jump up ^ The Committee Office, House of Commons (19 February 2009). "DFID's expenditure on development assistance". UK Parliament. Retrieved 28 April 2013. +Jump up ^ "Ministry of Defence". Ministry of Defence. Retrieved 21 February 2012. +Jump up ^ "Speaker addresses Her Majesty Queen Elizabeth II". UK Parliament. 30 March 2012. Retrieved 28 April 2013. +Jump up ^ "House of Commons Hansard". UK Parliament. Retrieved 23 October 2008. +Jump up ^ UK 2005: The Official Yearbook of the United Kingdom of Great Britain and Northern Ireland. Office for National Statistics. p. 89. +Jump up ^ "Principles for Economic Regulation". Department for Business, Innovation & Skills. April 2011. Retrieved 1 May 2011. +Jump up ^ "United Kingdom". International Monetary Fund. Retrieved 1 October 2009. +Jump up ^ Chavez-Dreyfuss, Gertrude (1 April 2008). "Global reserves, dollar share up at end of 2007-IMF". Reuters. Retrieved 21 December 2009. +Jump up ^ "More About the Bank". Bank of England. n.d. Archived from the original on 12 March 2008. +Jump up ^ "Index of Services (experimental)". Office for National Statistics. 7 May 2006. Archived from the original on 7 May 2006. +Jump up ^ Sassen, Saskia (2001). The Global City: New York, London, Tokyo (2nd ed.). Princeton University Press. ISBN 0-691-07866-1. +^ Jump up to: a b "Global Financial Centres 7". Z/Yen. 2010. Retrieved 21 April 2010. +^ Jump up to: a b "Worldwide Centres of Commerce Index 2008". Mastercard. Retrieved 5 July 2011. +^ Jump up to: a b Zumbrun, Joshua (15 July 2008). ""World's Most Economically Powerful Cities".". Forbes (New York). Archived from the original on 19 May 2011. Retrieved 3 October 2010. +Jump up ^ "Global city GDP rankings 2008–2025". PricewaterhouseCoopers. Archived from the original on 19 May 2011. Retrieved 16 November 2010. +Jump up ^ Lazarowicz, Mark (Labour MP) (30 April 2003). "Financial Services Industry". UK Parliament. Retrieved 17 October 2008. +Jump up ^ International Tourism Receipts[dead link]. UNWTO Tourism Highlights, Edition 2005. page 12. World Tourism Organisation. Retrieved 24 May 2006. +Jump up ^ Bremner, Caroline (10 January 2010). "Euromonitor International's Top City Destination Ranking". Euromonitor International. Archived from the original on 19 May 2011. Retrieved 31 May 2011. +Jump up ^ "From the Margins to the Mainstream – Government unveils new action plan for the creative industries". DCMS. 9 March 2007. Retrieved 9 March 2007.[dead link] +^ Jump up to: a b "European Countries – United Kingdom". Europa (web portal). Retrieved 15 December 2010. +Jump up ^ Harrington, James W.; Warf, Barney (1995). Industrial location: Principles, practices, and policy. London: Routledge. p. 121. ISBN 978-0-415-10479-1. +Jump up ^ Spielvogel, Jackson J. (2008). Western Civilization: Alternative Volume: Since 1300. Belmont, CA: Thomson Wadsworth. ISBN 978-0-495-55528-5. +Jump up ^ Hewitt, Patricia (15 July 2004). "TUC Manufacturing Conference". Department of Trade and Industry. Retrieved 16 May 2006. +Jump up ^ "Industry topics". Society of Motor Manufacturers and Traders. 2011. Retrieved 5 July 2011. +Jump up ^ Robertson, David (9 January 2009). "The Aerospace industry has thousands of jobs in peril". The Times (London). Retrieved 9 June 2011. (subscription required) +Jump up ^ "Facts & Figures – 2009". Aerospace & Defence Association of Europe. Retrieved 9 June 2011.[dead link] +Jump up ^ "UK Aerospace Industry Survey – 2010". ADS Group. Retrieved 9 June 2011. +^ Jump up to: a b c d http://www.theengineer.co.uk/aerospace/in-depth/reasons-to-be-cheerful-about-the-uk-aerospace-sector/1017274.article +Jump up ^ "The Pharmaceutical sector in the UK". Department for Business, Innovation & Skills. Retrieved 9 June 2011. +Jump up ^ "Ministerial Industry Strategy Group – Pharmaceutical Industry: Competitiveness and Performance Indicators". Department of Health. Retrieved 9 June 2011.[dead link] +Jump up ^ [1][dead link] +Jump up ^ "UK in recession as economy slides". BBC News. 23 January 2009. Retrieved 23 January 2009. +Jump up ^ "UK youth unemployment at its highest in two decades: 22.5%". MercoPress. 15 April 2012. +Jump up ^ Groom, Brian (19 January 2011). "UK youth unemployment reaches record". Financial Times (London). +Jump up ^ "Release: EU Government Debt and Deficit returns". Office for National Statistics. March 2012. Retrieved 17 August 2012. +Jump up ^ "UK loses top AAA credit rating for first time since 1978". BBC News. 23 February 2013. Retrieved 23 February 2013. +Jump up ^ "Britain sees real wages fall 3.2%". Daily Express (London). 2 March 2013. +Jump up ^ Beckford, Martin (5 December 2011). "Gap between rich and poor growing fastest in Britain". The Daily Telegraph (London). +Jump up ^ "United Kingdom: Numbers in low income". The Poverty Site. Retrieved 25 September 2009. +Jump up ^ "United Kingdom: Children in low income households". The Poverty Site. Retrieved 25 September 2009. +Jump up ^ "Warning of food price hike crisis". BBC News. 4 April 2009. +Jump up ^ Andrews, J. (16 January 2013). "How poor is Britain now". Yahoo! Finance UK +Jump up ^ Glynn, S.; Booth, A. (1996). Modern Britain: An Economic and Social History. London: Routledge. +Jump up ^ "Report highlights 'bleak' poverty levels in the UK" Phys.org, 29 March 2013 +Jump up ^ Gascoin, J. "A reappraisal of the role of the universities in the Scientific Revolution", in Lindberg, David C. and Westman, Robert S., eds (1990), Reappraisals of the Scientific Revolution. Cambridge University Press. p. 248. ISBN 0-521-34804-8. +Jump up ^ Reynolds, E.E.; Brasher, N.H. (1966). Britain in the Twentieth Century, 1900–1964. Cambridge University Press. p. 336. OCLC 474197910 +Jump up ^ Burtt, E.A. (2003) [1924].The Metaphysical Foundations of Modern Science. Mineola, NY: Courier Dover. p. 207. ISBN 0-486-42551-7. +Jump up ^ Hatt, C. (2006). Scientists and Their Discoveries. London: Evans Brothers. pp. 16, 30 and 46. ISBN 0-237-53195-X. +Jump up ^ Jungnickel, C.; McCormmach, R. (1996). Cavendish. American Philosophical Society. ISBN 0-87169-220-1. +Jump up ^ "The Nobel Prize in Physiology or Medicine 1945: Sir Alexander Fleming, Ernst B. Chain, Sir Howard Florey". The Nobel Foundation. Archived from the original on 21 June 2011. +Jump up ^ Hatt, C. (2006). Scientists and Their Discoveries. London: Evans Brothers. p. 56. ISBN 0-237-53195-X. +Jump up ^ James, I. (2010). Remarkable Engineers: From Riquet to Shannon. Cambridge University Press. pp. 33–6. ISBN 0-521-73165-8. +Jump up ^ Bova, Ben (2002) [1932]. The Story of Light. Naperville, IL: Sourcebooks. p. 238. ISBN 978-1-4022-0009-0. +Jump up ^ "Alexander Graham Bell (1847–1922)". Scottish Science Hall of Fame. Archived from the original on 21 June 2011. +Jump up ^ "John Logie Baird (1888–1946)". BBC History. Archived from the original on 21 June 2011. +Jump up ^ Cole, Jeffrey (2011). Ethnic Groups of Europe: An Encyclopedia. Santa Barbara, CA: ABC-CLIO. p. 121. ISBN 1-59884-302-8. +Jump up ^ Castells, M.; Hall, P.; Hall, P.G. (2004). Technopoles of the World: the Making of Twenty-First-Century Industrial Complexes. London: Routledge. pp. 98–100. ISBN 0-415-10015-1. +Jump up ^ "Knowledge, networks and nations: scientific collaborations in the twenty-first century". Royal Society. 2011. Archived from the original on 22 June 2011. +Jump up ^ McCook, Alison. "Is peer review broken?". Reprinted from the Scientist 20(2) 26, 2006. Archived from the original on 21 June 2011. +^ Jump up to: a b "Heathrow 'needs a third runway'". BBC News. 25 June 2008. Retrieved 17 October 2008. +^ Jump up to: a b "Statistics: Top 30 World airports" (Press release). Airports Council International. July 2008. Retrieved 15 October 2008. +Jump up ^ "Transport Statistics Great Britain: 2010". Department for Transport. Archived from the original on 16 December 2010. +Jump up ^ "Major new rail lines considered". BBC News. 21 June 2008. Archived from the original on 9 October 2010. +Jump up ^ "Crossrail's giant tunnelling machines unveiled". BBC News. 2 January 2012. +Jump up ^ Leftly, Mark (29 August 2010). "Crossrail delayed to save £1bn". The Independent on Sunday (London). +^ Jump up to: a b "Size of Reporting Airports October 2009 – September 2010". Civil Aviation Authority. Retrieved 5 December 2010. +Jump up ^ "BMI being taken over by Lufthansa". BBC News. 29 October 2008. Retrieved 23 December 2009. +Jump up ^ "United Kingdom Energy Profile". U.S. Energy Information Administration. Retrieved 4 November 2010. +Jump up ^ Mason, Rowena (24 October 2009). "Let the battle begin over black gold". The Daily Telegraph (London). Retrieved 26 November 2010. +Jump up ^ Heath, Michael (26 November 2010). "RBA Says Currency Containing Prices, Rate Level 'Appropriate' in Near Term". Bloomberg (New York). Retrieved 26 November 2010. +^ Jump up to: a b c "Nuclear Power in the United Kingdom". World Nuclear Association. April 2013. Retrieved 9 April 2013. +^ Jump up to: a b c "United Kingdom – Oil". U.S. Energy Information Administration. Retrieved 4 November 2010.[dead link] +Jump up ^ "Diminishing domestic reserves, escalating imports". EDF Energy. Retrieved 9 April 2013. +^ Jump up to: a b "United Kingdom – Natural Gas". U.S. Energy Information Administration. Retrieved 4 November 2010.[dead link] +^ Jump up to: a b "United Kingdom – Quick Facts Energy Overview". U.S. Energy Information Administration. Retrieved 4 November 2010.[dead link] +Jump up ^ The Coal Authority (10 April 2006). "Coal Reserves in the United Kingdom". The Coal Authority. Archived from the original on 4 January 2009. Retrieved 5 July 2011. +Jump up ^ "England Expert predicts 'coal revolution'". BBC News. 16 October 2007. Retrieved 23 September 2008. +Jump up ^ Watts, Susan (20 March 2012). "Fracking: Concerns over gas extraction regulations". BBC News. Retrieved 9 April 2013. +Jump up ^ "Quit fracking aboot". Friends of the Earth Scotland. Retrieved 9 April 2013. +Jump up ^ "Census Geography". Office for National Statistics. 30 October 2007. Archived from the original on 4 June 2011. Retrieved 14 April 2012. +Jump up ^ "Welcome to the 2011 Census for England and Wales". Office for National Statistics. n.d. Retrieved 11 October 2008. +^ Jump up to: a b c "2011 Census: Population Estimates for the United Kingdom". Office for National Statistics. 27 March 2011. Retrieved 18 December 2012. +^ Jump up to: a b c "Annual Mid-year Population Estimates, 2010". Office for National Statistics. 2011. Retrieved 14 April 2012. +Jump up ^ Batty, David (30 December 2010). "One in six people in the UK today will live to 100, study says". The Guardian (London). +^ Jump up to: a b "2011 UK censuses". Office for National Statistics. Retrieved 18 December 2012. +Jump up ^ "Population: UK population grows to 59.6 million" (Press release). Office for National Statistics. 24 June 2004. Archived from the original on 22 July 2004. Retrieved 14 April 2012. +Jump up ^ Khan, Urmee (16 September 2008). "England is most crowded country in Europe". The Daily Telegraph (London). Retrieved 5 September 2009. +Jump up ^ Carrell, Severin (17 December 2012). "Scotland's population at record high". The Guardian. London. Retrieved 18 December 2012. +^ Jump up to: a b c "Vital Statistics: Population and Health Reference Tables (February 2014 Update): Annual Time Series Data". ONS. Retrieved 27 April 2014. +Jump up ^ Boseley, Sarah (14 July 2008). "The question: What's behind the baby boom?". The Guardian (London). p. 3. Retrieved 28 August 2009. +Jump up ^ Tables, Graphs and Maps Interface (TGM) table. Eurostat (26 February 2013). Retrieved 12 July 2013. +Jump up ^ Campbell, Denis (11 December 2005). "3.6m people in Britain are gay – official". The Observer (London). Retrieved 28 April 2013. +Jump up ^ "2011 Census - Built-up areas". ONS. Retrieved 1 July 2013. +Jump up ^ Mid-2012 Population Estimates for Settlements and Localities in Scotland General Register Office for Scotland +Jump up ^ "Belfast Metropolitan Urban Area NISRA 2005". Retrieved 28 April 2013. +Jump up ^ 2011 Census: KS201UK Ethnic group, local authorities in the United Kingdom, Accessed 21 February 2014 +Jump up ^ "Welsh people could be most ancient in UK, DNA suggests". BBC News. 19 June 2012. Retrieved 28 April 2013. +Jump up ^ Thomas, Mark G. et al. "Evidence for a segregated social structure in early Anglo-Saxon England". Proceedings of the Royal Society B: Biological Sciences 273(1601): 2651–2657. +Jump up ^ Owen, James (19 July 2005). "Review of 'The Tribes of Britain'". National Geographic (Washington DC). +Jump up ^ Oppenheimer, Stephen (October 2006). "Myths of British ancestry" at the Wayback Machine (archived 26 September 2006). Prospect (London). Retrieved 5 November 2010. +Jump up ^ Henderson, Mark (23 October 2009). "Scientist – Griffin hijacked my work to make race claim about 'British aborigines'". The Times (London). Retrieved 26 October 2009. (subscription required) +Jump up ^ Costello, Ray (2001). Black Liverpool: The Early History of Britain's Oldest Black Community 1730–1918. Liverpool: Picton Press. ISBN 1-873245-07-6. +Jump up ^ "Culture and Ethnicity Differences in Liverpool – Chinese Community". Chambré Hardman Trust. Retrieved 26 October 2009. +Jump up ^ Coleman, David; Compton, Paul; Salt, John (2002). "The demographic characteristics of immigrant populations", Council of Europe, p.505. ISBN 92-871-4974-7. +Jump up ^ Mason, Chris (30 April 2008). "'Why I left UK to return to Poland'". BBC News. +Jump up ^ "Resident population estimates by ethnic group (percentages): London". Office for National Statistics. Retrieved 23 April 2008. +Jump up ^ "Resident population estimates by ethnic group (percentages): Leicester". Office for National Statistics. Retrieved 23 April 2008. +Jump up ^ "Census 2001 – Ethnicity and religion in England and Wales". Office for National Statistics. Retrieved 23 April 2008. +Jump up ^ Loveys, Kate (22 June 2011). "One in four primary school pupils are from an ethnic minority and almost a million schoolchildren do not speak English as their first language". Daily Mail (London). Retrieved 28 June 2011. +Jump up ^ Rogers, Simon (19 May 2011). "Non-white British population reaches 9.1 million". The Guardian (London). +Jump up ^ Wallop, Harry (18 May 2011). "Population growth of last decade driven by non-white British". The Daily Telegraph (London). +Jump up ^ "Official EU languages". European Commission. 8 May 2009. Retrieved 16 October 2009. +Jump up ^ "Language Courses in New York". United Nations. 2006. Retrieved 29 November 2010. +Jump up ^ "English language – Government, citizens and rights". Directgov. Retrieved 23 August 2011. +Jump up ^ "Commonwealth Secretariat – UK". Commonwealth Secretariat. Retrieved 23 August 2011. +^ Jump up to: a b c "Languages across Europe: United Kingdom". BBC. Retrieved 4 February 2013. +Jump up ^ Booth, Robert (30 January 2013). "Polish becomes England's second language". The Guardian (London). Retrieved 4 February 2012. +Jump up ^ European Charter for Regional or Minority Languages, Strasbourg, 5.XI.1992 - http://conventions.coe.int/treaty/en/Treaties/Html/148.htm +Jump up ^ Framework Convention for the Protection of National Minorities, Strasbourg, 1.II.1995 - http://conventions.coe.int/Treaty/en/Treaties/Html/157.htm +Jump up ^ National Statistics Online – Welsh Language[dead link]. National Statistics Office. +Jump up ^ "Differences in estimates of Welsh Language Skills". Office for National Statistics. Archived from the original on 12 January 2010. Retrieved 30 December 2008. +Jump up ^ Wynn Thomas, Peter (March 2007). "Welsh today". Voices. BBC. Retrieved 5 July 2011. +Jump up ^ "Scotland's Census 2001 – Gaelic Report". General Register Office for Scotland. Retrieved 28 April 2013. +Jump up ^ "Local UK languages 'taking off'". BBC News. 12 February 2009. +Jump up ^ Edwards, John R. (2010). Minority languages and group identity: cases and categories. John Benjamins. pp. 150–158. ISBN 978-90-272-1866-7. Retrieved 12 March 2011. +Jump up ^ Koch, John T. (2006). Celtic culture: a historical encyclopedia. ABC-CLIO. p. 696. ISBN 978-1-85109-440-0. +Jump up ^ "Language Data – Scots". European Bureau for Lesser-Used Languages. Archived from the original on 23 June 2007. Retrieved 2 November 2008. +Jump up ^ "Fall in compulsory language lessons". BBC News. 4 November 2004. +Jump up ^ "The School Gate for parents in Wales". BBC. Retrieved 28 April 2013. +Jump up ^ Cannon, John, ed. (2nd edn., 2009). A Dictionary of British History. Oxford University Press. p. 144. ISBN 0-19-955037-9. +Jump up ^ Field, Clive D. (November 2009). "British religion in numbers"[dead link]. BRIN Discussion Series on Religious Statistics, Discussion Paper 001. Retrieved 3 June 2011. +Jump up ^ Yilmaz, Ihsan (2005). Muslim Laws, Politics and Society in Modern Nation States: Dynamic Legal Pluralisms in England, Turkey, and Pakistan. Aldershot: Ashgate Publishing. pp. 55–6. ISBN 0-7546-4389-1. +Jump up ^ Brown, Callum G. (2006). Religion and Society in Twentieth-Century Britain. Harlow: Pearson Education. p. 291. ISBN 0-582-47289-X. +Jump up ^ Norris, Pippa; Inglehart, Ronald (2004). Sacred and Secular: Religion and Politics Worldwide. Cambridge University Press. p. 84. ISBN 0-521-83984-X. +Jump up ^ Fergusson, David (2004). Church, State and Civil Society. Cambridge University Press. p. 94. ISBN 0-521-52959-X. +Jump up ^ "UK Census 2001". National Office for Statistics. Archived from the original on 12 March 2007. Retrieved 22 April 2007. +Jump up ^ "Religious Populations". Office for National Statistics. 11 October 2004. Archived from the original on 6 June 2011. +Jump up ^ "United Kingdom: New Report Finds Only One in 10 Attend Church". News.adventist.org. 4 April 2007. Retrieved 12 September 2010. +Jump up ^ Philby, Charlotte (12 December 2012). "Less religious and more ethnically diverse: Census reveals a picture of Britain today". The Independent (London). +Jump up ^ The History of the Church of England. The Church of England. Retrieved 23 November 2008. +Jump up ^ "Queen and Church of England". British Monarchy Media Centre. Archived from the original on 8 October 2006. Retrieved 5 June 2010. +Jump up ^ "Queen and the Church". The British Monarchy (Official Website). Archived from the original on 7 June 2011. +Jump up ^ "How we are organised". Church of Scotland. Archived from the original on 7 June 2011. +Jump up ^ Weller, Paul (2005). Time for a Change: Reconfiguring Religion, State, and Society. London: Continuum. pp. 79–80. ISBN 0567084876. +Jump up ^ Peach, Ceri, "United Kingdom, a major transformation of the religious landscape", in H. Knippenberg. ed. (2005). The Changing Religious Landscape of Europe. Amsterdam: Het Spinhuis. pp. 44–58. ISBN 90-5589-248-3. +Jump up ^ Richards, Eric (2004). Britannia's children: Emigration from England, Scotland, Wales and Ireland since 1600. London: Hambledon, p. 143. ISBN 978-1-85285-441-6. +Jump up ^ Gibney, Matthew J.; Hansen, Randall (2005). Immigration and asylum: from 1900 to the present, ABC-CLIO, p. 630. ISBN 1-57607-796-9 +Jump up ^ "Short history of immigration". BBC. 2005. Retrieved 28 August 2010. +Jump up ^ Rogers, Simon (11 December 2012). "Census 2011 mapped and charted: England & Wales in religion, immigration and race". London: Guardian. Retrieved 11 December 2012. +Jump up ^ 6.5% of the EU population are foreigners and 9.4% are born abroad, Eurostat, Katya Vasileva, 34/2011. +Jump up ^ Muenz, Rainer (June 2006). "Europe: Population and Migration in 2005". Migration Policy Institute. Retrieved 2 April 2007. +Jump up ^ "Immigration and births to non-British mothers pushes British population to record high". London Evening Standard. 21 August 2008. +Jump up ^ Doughty, Steve; Slack, James (3 June 2008). "Third World migrants behind our 2.3m population boom". Daily Mail (London). +Jump up ^ Bentham, Martin (20 October 2008). "Tories call for tougher control of immigration". London Evening Standard. +Jump up ^ "Minister rejects migrant cap plan". BBC News. 8 September 2008. Retrieved 26 April 2011. +Jump up ^ Johnston, Philip (5 January 2007). "Immigration 'far higher' than figures say". The Daily Telegraph (London). Retrieved 20 April 2007. +Jump up ^ Travis, Alan (25 August 2011). "UK net migration rises 21%". The Guardian (London). +^ Jump up to: a b "Migration Statistics Quarterly Report May 2012". Office for National Statistics. 24 May 2012. +Jump up ^ "Migration to UK more than double government target". BBC News. 24 May 2012. +^ Jump up to: a b "Citizenship". Home Office. August 2011. Retrieved 24 October 2011.[dead link] +Jump up ^ Bamber, David (20 December 2000). "Migrant squad to operate in France". The Daily Telegraph (London). +Jump up ^ "Settlement". Home Office. August 2011. Retrieved 24 October 2011.[dead link] +Jump up ^ "Births in England and Wales by parents' country of birth, 2011". Office for National Statistics. 30 August 2012. Retrieved 28 April 2013. +Jump up ^ "Right of Union citizens and their family members to move and reside freely within the territory of the Member States". European Commission. Retrieved 28 April 2013. +Jump up ^ Doward, Jamie; Temko, Ned (23 September 2007). "Home Office shuts the door on Bulgaria and Romania". The Observer (London). p. 2. Retrieved 23 August 2008. +Jump up ^ Sumption, Madeleine; Somerville, Will (January 2010). The UK's new Europeans: Progress and challenges five years after accession. Policy Report (London: Equality and Human Rights Commission). p. 13. ISBN 978-1-84206-252-4. Retrieved 19 January 2010. +Jump up ^ Doward, Jamie; Rogers, Sam (17 January 2010). "Young, self-reliant, educated: portrait of UK's eastern European migrants". The Observer (London). Retrieved 19 January 2010. +Jump up ^ Hopkirk, Elizabeth (20 October 2008). "Packing up for home: Poles hit by UK's economic downturn". London Evening Standard. +Jump up ^ "Migrants to UK 'returning home'". BBC News. 8 September 2009. Retrieved 8 September 2009. +Jump up ^ "UK sees shift in migration trend". BBC News. 27 May 2010. Retrieved 28 May 2010. +Jump up ^ "Fresh Talent: Working in Scotland". London: UK Border Agency. Retrieved 30 October 2010. +Jump up ^ Boxell, James (28 June 2010). "Tories begin consultation on cap for migrants". Financial Times (London). Retrieved 17 September 2010. +Jump up ^ "Vince Cable: Migrant cap is hurting economy". The Guardian (London). Press Association. 17 September 2010. Retrieved 17 September 2010. +Jump up ^ Richards (2004), pp. 6–7. +^ Jump up to: a b Sriskandarajah, Dhananjayan; Drew, Catherine (11 December 2006). "Brits Abroad: Mapping the scale and nature of British emigration". Institute for Public Policy Research. Retrieved 20 January 2007. +Jump up ^ "Brits Abroad: world overview". BBC. n.d. Retrieved 20 April 2007. +Jump up ^ Casciani, Dominic (11 December 2006). "5.5 m Britons 'opt to live abroad'". BBC News. Retrieved 20 April 2007. +Jump up ^ "Brits Abroad: Country-by-country". BBC News. 11 December 2006. +Jump up ^ "Local Authorities". Department for Children, Schools and Families. Retrieved 21 December 2008. +Jump up ^ Gordon, J.C.B. (1981). Verbal Deficit: A Critique. London: Croom Helm. p. 44 note 18. ISBN 978-0-85664-990-5. +Jump up ^ Section 8 ('Duty of local education authorities to secure provision of primary and secondary schools'), Sections 35–40 ('Compulsory attendance at Primary and Secondary Schools') and Section 61 ('Prohibition of fees in schools maintained by local education authorities ...'), Education Act 1944. +Jump up ^ "England's pupils in global top 10". BBC News. 10 December 2008. +Jump up ^ "More state pupils in universities". BBC News. 19 July 2007. +Jump up ^ MacLeod, Donald (9 November 2007). "Private school pupil numbers in decline". The Guardian (London). Retrieved 31 March 2010. +Jump up ^ Frankel, Hannah (3 September 2010). "Is Oxbridge still a preserve of the posh?". TES (London). Retrieved 9 April 2013. +Jump up ^ "World's top 100 universities 2013: their reputations ranked by Times Higher Education". The Guardian (London). 2013. Retrieved 23 October 2014. +Jump up ^ Davenport, F.; Beech, C.; Downs, T.; Hannigan, D. (2006). Ireland. Lonely Planet, 7th edn. ISBN 1-74059-968-3. p. 564. +Jump up ^ "About SQA". Scottish Qualifications Authority. 10 April 2013. Retrieved 28 April 2013. +Jump up ^ "About Learning and Teaching Scotland". Learning and Teaching Scotland. Retrieved 28 April 2013. +Jump up ^ "Brain drain in reverse". Scotland Online Gateway. July 2002. Archived from the original on 4 December 2007. +Jump up ^ "Increase in private school intake". BBC News. 17 April 2007. +Jump up ^ "MSPs vote to scrap endowment fee". BBC News. 28 February 2008. +Jump up ^ What will your child learn?[dead link] The Welsh Assembly Government. Retrieved 22 January 2010. +Jump up ^ CCEA. "About Us – What we do". Council for the Curriculum Examinations & Assessment. Retrieved 28 April 2013. +Jump up ^ Elitist Britain?, Social Mobility and Child Poverty Commission, 28 August 2014 +Jump up ^ Arnett, George (28 August 2014). "Elitism in Britain - breakdown by profession". The Guardian: Datablog. +Jump up ^ Haden, Angela; Campanini, Barbara, eds. (2000). The world health report 2000 – Health systems: improving performance. Geneva: World Health Organisation. ISBN 92-4-156198-X. Retrieved 5 July 2011. +Jump up ^ World Health Organization. "Measuring overall health system performance for 191 countries". New York University. Retrieved 5 July 2011. +Jump up ^ "'Huge contrasts' in devolved NHS". BBC News. 28 August 2008. +Jump up ^ Triggle, Nick (2 January 2008). "NHS now four different systems". BBC News. +Jump up ^ Fisher, Peter. "The NHS from Thatcher to Blair". NHS Consultants Association (International Association of Health Policy). The Budget ... was even more generous to the NHS than had been expected amounting to an annual rise of 7.4% above the rate of inflation for the next 5 years. This would take us to 9.4% of GDP spent on health ie around EU average. +Jump up ^ "OECD Health Data 2009 – How Does the United Kingdom Compare". Paris: Organisation for Economic Co-operation and Development. Retrieved 28 April 2013.[dead link] +Jump up ^ "The cultural superpower: British cultural projection abroad". Journal of the British Politics Society, Norway. Volume 6. No. 1. Winter 2011 +Jump up ^ Sheridan, Greg (15 May 2010). "Cameron has chance to make UK great again". The Australian (Sydney). Retrieved 20 May 2012. +Jump up ^ Goldfarb, Jeffrey (10 May 2006). "Bookish Britain overtakes America as top publisher". RedOrbit (Texas). Reuters. +Jump up ^ "William Shakespeare (English author)". Britannica Online encyclopedia. Retrieved 26 February 2006. +Jump up ^ MSN Encarta Encyclopedia article on Shakespeare. Archived from the original on 9 February 2006. Retrieved 26 February 2006. +Jump up ^ William Shakespeare. Columbia Electronic Encyclopedia. Retrieved 26 February 2006. +Jump up ^ "Mystery of Christie's success is solved". The Daily Telegraph (London). 19 December 2005. Retrieved 14 November 2010. +Jump up ^ "All-Time Essential Comics". IGN. Retrieved 15 August 2013. +Jump up ^ Johnston, Rich."Before Watchmen To Double Up For Hardcover Collections". Bleeding Cool. 10 December 2012. Retrieved 15 August 2013. +Jump up ^ "Edinburgh, UK appointed first UNESCO City of Literature". Unesco. 2004. Retrieved 28 April 2013.[dead link] +Jump up ^ "Early Welsh poetry". BBC Wales. Retrieved 29 December 2010. +Jump up ^ Lang, Andrew (2003) [1913]. History of English Literature from Beowulf to Swinburne. Holicong, PA: Wildside Press. p. 42. ISBN 978-0-8095-3229-2. +Jump up ^ "Dafydd ap Gwilym". Academi website. Academi. 2011. Retrieved 3 January 2011. Dafydd ap Gwilym is widely regarded as one of the greatest Welsh poets of all time, and amongst the leading European poets of the Middle Ages. +Jump up ^ True birthplace of Wales's literary hero. BBC News. Retrieved 28 April 2012 +Jump up ^ Kate Roberts: Biography at the Wayback Machine. BBC Wales. Retrieved 28 April 2012 +Jump up ^ Swift, Jonathan; Fox, Christopher (1995). Gulliver's travels: complete, authoritative text with biographical and historical contexts, critical history, and essays from five contemporary critical perspectives. Basingstoke: Macmillan. p. 10. ISBN 978-0-333-63438-7. +Jump up ^ "Bram Stoker." (PDF). The New York Times. 23 April 1912. Retrieved 1 January 2011. +^ Jump up to: a b "1960–1969". EMI Group. Retrieved 31 May 2008. +^ Jump up to: a b "Paul At Fifty". Time (New York). 8 June 1992. +^ Jump up to: a b Most Successful Group The Guinness Book of Records 1999, p. 230. Retrieved 19 March 2011. +Jump up ^ "British Citizen by Act of Parliament: George Frideric Handel". UK Parliament. 20 July 2009. Retrieved 11 September 2009.[dead link] +Jump up ^ Andrews, John (14 April 2006). "Handel all'inglese". Playbill (New York). Retrieved 11 September 2009. +Jump up ^ Citron, Stephen (2001). Sondheim and Lloyd-Webber: The new musical. London: Chatto & Windus. ISBN 978-1-85619-273-6. +Jump up ^ "Beatles a big hit with downloads". Belfast Telegraph. 25 November 2010. Retrieved 16 May 2011. +Jump up ^ "British rock legends get their own music title for PlayStation3 and PlayStation2" (Press release). EMI. 2 February 2009. +Jump up ^ Khan, Urmee (17 July 2008). "Sir Elton John honoured in Ben and Jerry ice cream". The Daily Telegraph (London). +Jump up ^ Alleyne, Richard (19 April 2008). "Rock group Led Zeppelin to reunite". The Daily Telegraph (London). Retrieved 31 March 2010. +Jump up ^ Fresco, Adam (11 July 2006). "Pink Floyd founder Syd Barrett dies at home". The Times (London). Retrieved 31 March 2010. (subscription required) +Jump up ^ Holton, Kate (17 January 2008). "Rolling Stones sign Universal album deal". Reuters. Retrieved 26 October 2008. +Jump up ^ Walker, Tim (12 May 2008). "Jive talkin': Why Robin Gibb wants more respect for the Bee Gees". The Independent (London). Retrieved 26 October 2008. +Jump up ^ "Brit awards winners list 2012: every winner since 1977". The Guardian (London). Retrieved 28 February 2012. +Jump up ^ Corner, Lewis (16 February 2012). "Adele, Coldplay biggest-selling UK artists worldwide in 2011". Digital Spy. Retrieved 22 March 2012. +Jump up ^ Hughes, Mark (14 January 2008). "A tale of two cities of culture: Liverpool vs Stavanger". The Independent (London). Retrieved 2 August 2009. +Jump up ^ "Glasgow gets city of music honour". BBC News. 20 August 2008. Retrieved 2 August 2009. +Jump up ^ Bayley, Stephen (24 April 2010). "The startling success of Tate Modern". The Times (London). Retrieved 19 January 2011. (subscription required) +Jump up ^ "Vertigo is named 'greatest film of all time'". BBC News. 2 August 2012. Retrieved 18 August 2012. +Jump up ^ "The Directors' Top Ten Directors". British Film Institute. Archived from the original on 27 May 2012. +Jump up ^ "Chaplin, Charles (1889–1977)". British Film Institute. Retrieved 25 January 2011. +Jump up ^ "Powell, Michael (1905–1990)". British Film Institute. Retrieved 25 January 2011. +Jump up ^ "Reed, Carol (1906–1976)". British Film Institute. Retrieved 25 January 2011. +Jump up ^ "Scott, Sir Ridley (1937–)". British Film Institute. Retrieved 25 January 2011. +Jump up ^ "Andrews, Julie (1935–)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Burton, Richard (1925–1984)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Caine, Michael (1933–)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Chaplin, Charles (1889–1977)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Connery, Sean (1930–)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Leigh, Vivien (1913–1967)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Niven, David (1910–1983)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Olivier, Laurence (1907–1989)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Sellers, Peter (1925–1980)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Winslet, Kate (1975–)". British Film Institute. Retrieved 11 December 2010. +Jump up ^ "Daniel Day-Lewis makes Oscar history with third award"'. BBC News. Retrieved 15 August 2013 +Jump up ^ "Harry Potter becomes highest-grossing film franchise". The Guardian (London). 11 September 2007. Retrieved 2 November 2010. +Jump up ^ "History of Ealing Studios". Ealing Studios. Retrieved 5 June 2010. +^ Jump up to: a b "UK film – the vital statistics". UK Film Council. Retrieved 22 October 2010.[dead link] +Jump up ^ "The BFI 100". British Film Institute. 6 September 2006. Archived from the original on 1 April 2011. +Jump up ^ "Baftas fuel Oscars race". BBC News. 26 February 2001. Retrieved 14 February 2011. +^ Jump up to: a b "BBC: World's largest broadcaster & Most trusted media brand". Media Newsline. Archived from the original on 5 October 2010. Retrieved 23 September 2010. +^ Jump up to: a b "Digital licence". Prospect. Retrieved 23 September 2010. +^ Jump up to: a b "About the BBC – What is the BBC". BBC Online. Retrieved 23 September 2010. +Jump up ^ Newswire7 (13 August 2009). "BBC: World's largest broadcaster & Most trusted media brand". Media Newsline. Archived from the original on 17 June 2011. +Jump up ^ "TV Licence Fee: facts & figures". BBC Press Office. April 2010. Archived from the original on 17 June 2011. +Jump up ^ "Publications & Policies: The History of ITV". ITV.com. Archived from the original on 17 June 2011. +Jump up ^ "Publishing". News Corporation. Archived from the original on 17 June 2011. +Jump up ^ "Direct Broadcast Satellite Television". News Corporation. Archived from the original on 17 June 2011. +Jump up ^ William, D. (2010). UK Cities: A Look at Life and Major Cities in England, Scotland, Wales and Northern Ireland. Eastbourne: Gardners Books. ISBN 978-9987-16-021-1, pp. 22, 46, 109 and 145. +Jump up ^ "Publishing". Department of Culture, Media and Sport. Archived from the original on 17 June 2011. +Jump up ^ Ofcom "Communication Market Report 2010", 19 August 2010, pp. 97, 164 and 191 +Jump up ^ "Social Trends: Lifestyles and social participation". Office for National Statistics. 16 February 2010. Archived from the original on 17 June 2011. +Jump up ^ "Top 20 countries with the highest number of Internet users". Internet World Stats. Archived from the original on 17 June 2011. +Jump up ^ Fieser, James, ed. (2000). A bibliography of Scottish common sense philosophy: Sources and origins. Bristol: Thoemmes Press. Retrieved 17 December 2010. +Jump up ^ Palmer, Michael (1999). Moral Problems in Medicine: A Practical Coursebook. Cambridge: Lutterworth Press. p. 66. ISBN 978-0-7188-2978-0. +Jump up ^ Scarre, Geoffrey (1995). Utilitarianism. London: Routledge. p. 82. ISBN 978-0-415-12197-2. +Jump up ^ Gysin, Christian (9 March 2007). "Wembley kick-off: Stadium is ready and England play first game in fortnight". Daily Mail (London). Retrieved 19 March 2007. +Jump up ^ "Opening ceremony of the games of the XXX Olympiad". Olympic.org. Retrieved 30 November 2013 +Jump up ^ "Unparalleled Sporting History" . Reuters. Retrieved 30 November 2013 +Jump up ^ "Rugby Union 'Britain's Second Most Popular Sport'". Ipsos-Mori. 22 December 2003. Retrieved 28 April 2013. +Jump up ^ Ebner, Sarah (2 July 2013). "History and time are key to power of football, says Premier League chief". The Times (London). Retrieved 30 November 2013. +Jump up ^ Mitchell, Paul (November 2005). "The first international football match". BBC Sport Scotland. Retrieved 15 December 2013. +Jump up ^ "Why is there no GB Olympics football team?". BBC Sport. 5 August 2008. Retrieved 31 December 2010. +Jump up ^ "Blatter against British 2012 team". BBC News. 9 March 2008. Retrieved 2 April 2008. +Jump up ^ "About ECB". England and Wales Cricket Board. n.d. Retrieved 28 April 2013. +Jump up ^ McLaughlin, Martyn (4 August 2009). "Howzat happen? England fields a Gaelic-speaking Scotsman in Ashes". The Scotsman (Edinburgh). Retrieved 30 December 2010. +Jump up ^ "Uncapped Joyce wins Ashes call up". BBC Sport. 15 November 2006. Retrieved 30 December 2010. +Jump up ^ "Glamorgan". BBC South East Wales. August 2009. Retrieved 30 December 2010. +Jump up ^ Ardener, Shirley (2007). Professional identities: policy and practice in business and bureaucracy. New York: Berghahn. p. 27. ISBN 978-1-84545-054-0. +Jump up ^ "Official Website of Rugby League World Cup 2008". Archived from the original on 16 October 2007. +Jump up ^ Louw, Jaco; Nesbit, Derrick (2008). The Girlfriends Guide to Rugby. Johannesburg: South Publishers. ISBN 978-0-620-39541-0. +Jump up ^ "Triple Crown". RBS 6 Nations. Retrieved 6 March 2011. +Jump up ^ "Tracking the Field". Ipsos MORI. Archived from the original on 5 February 2009. Retrieved 17 October 2008. +Jump up ^ "Links plays into the record books". BBC News. 17 March 2009. +Jump up ^ Chowdhury, Saj (22 January 2007). "China in Ding's hands". BBC Sport. Retrieved 2 January 2011. +Jump up ^ "Lawn Tennis and Major T.Gem". The Birmingham Civic Society. Archived from the original on 18 August 2011. Retrieved 31 December 2010. +Jump up ^ Gould, Joe (10 April 2007). "The ancient Irish sport of hurling catches on in America". Columbia News Service (Columbia Journalism School). Retrieved 17 May 2011. +Jump up ^ "Shinty". Scottishsport.co.uk. Retrieved 28 April 2013. +Jump up ^ "Welsh dragon call for Union flag". BBC News. 27 November 2007. Retrieved 17 October 2008. +Jump up ^ "Britannia on British Coins". Chard. Retrieved 25 June 2006. +Jump up ^ Baker, Steve (2001). Picturing the Beast. University of Illinois Press. p. 52. ISBN 0-252-07030-5. +Further reading +Hitchens, Peter (2000). The Abolition of Britain: from Winston Churchill to Princess Diana. Second ed. San Francisco, Calif.: Encounter Books. xi, 332 p. ISBN 1-893554-18-X. +Lambert, Richard S. (1964). The Great Heritage: a History of Britain for Canadians. House of Grant, 1964 (and earlier editions and/or printings). +External links +Find more about +United Kingdom +at Wikipedia's sister projects +Search Wiktionary Definitions from Wiktionary +Search Commons Media from Commons +Search Wikinews News stories from Wikinews +Search Wikiquote Quotations from Wikiquote +Search Wikisource Source texts from Wikisource +Search Wikibooks Textbooks from Wikibooks +Search Wikivoyage Travel guide from Wikivoyage +Search Wikiversity Learning resources from Wikiversity +Government +Official website of HM Government +Official website of the British Monarchy +Official Yearbook of the United Kingdom statistics +The official site of the British Prime Minister's Office +General information +United Kingdom from the BBC News +United Kingdom entry at The World Factbook +United Kingdom from UCB Libraries GovPubs +United Kingdom at DMOZ +United Kingdom Encyclopædia Britannica entry +United Kingdom from the OECD +United Kingdom at the EU + Wikimedia Atlas of United Kingdom + Geographic data related to United Kingdom at OpenStreetMap +Key Development Forecasts for the United Kingdom from International Futures +Travel +Official tourist guide to Britain +[hide] v t e +United Kingdom topics +History +Chronology +Formation Georgian era Victorian era Edwardian era World War I Interwar World War II UK since 1945 (Postwar Britain) +By topic +Economic Empire Maritime Military +Geography +Administrative +Countries of the United Kingdom Crown dependencies Overseas territories City status Towns Former colonies +Physical +British Isles terminology Great Britain Geology Northern Ireland Lakes and lochs Mountains Rivers Volcanoes +Resources +Energy/Renewable energy Biodiesel Coal Geothermal Hydraulic frac. Hydroelectricity Marine North Sea oil Solar Wind Food Agriculture Fishing English Scottish Hunting Materials Flora Forestry Mining +Politics +Constitution Courts Elections Foreign relations Judiciary Law Law enforcement Legislation Monarchy monarchs Nationality Parliament House of Commons House of Lords Political parties +Government +Cabinet list Civil service Departments Prime Minister list +Military +Royal Navy Army Royal Air Force Weapons of mass destruction +Economy +Banks Bank of England Budget Economic geography Pound (currency) Stock Exchange Taxation Telecommunications Tourism Transport +Society +Affordability of housing Crime Demography Drug policy Education Ethnic groups Health care Immigration Languages Poverty Food banks Prostitution Public holidays Social care Social structure +Culture +Art Cinema Cuisine Identity Literature Media television Music Religion Sport Symbols Theatre +[show] +Countries of the United Kingdom +Outline Index +Book Category Portal WikiProject +[show] +Gnome-globe.svg Geographic locale +[show] v t e +Member states of the European Union +[show] +International organisations +[show] v t e +English-speaking world +[show] v t e +National personifications +Coordinates: 55°N 3°W +Categories: United KingdomBritish IslandsConstitutional monarchiesCountries in EuropeEnglish-speaking countries and territoriesG20 nationsG7 nationsG8 nationsIsland countriesLiberal democraciesMember states of NATOMember states of the Commonwealth of NationsMember states of the Council of EuropeMember states of the European UnionMember states of the Union for the MediterraneanMember states of the United NationsNorthern EuropeWestern Europe +Navigation menu +Create accountLog inArticleTalkReadView sourceView history + +Main page +Contents +Featured content +Current events +Random article +Donate to Wikipedia +Wikimedia Shop +Interaction +Help +About Wikipedia +Community portal +Recent changes +Contact page +Tools +What links here +Related changes +Upload file +Special pages +Permanent link +Page information +Wikidata item +Cite this page +Print/export +Create a book +Download as PDF +Printable version +Languages +Адыгэбзэ +Afrikaans +Akan +Alemannisch +አማርኛ +Ænglisc +Аҧсшәа +العربية +Aragonés +ܐܪܡܝܐ +Armãneashti +Arpetan +Asturianu +Avañe'ẽ +Авар +Azərbaycanca +বাংলা +Bahasa Banjar +Bân-lâm-gú +Башҡортса +Беларуская +Беларуская (тарашкевіца)‎ +भोजपुरी +Bikol Central +Bislama +Български +Boarisch +བོད་ཡིག +Bosanski +Brezhoneg +Буряад +Català +Чӑвашла +Cebuano +Čeština +Chavacano de Zamboanga +ChiShona +Corsu +Cymraeg +Dansk +Deutsch +ދިވެހިބަސް +Diné bizaad +Dolnoserbski +ཇོང་ཁ +Eesti +Ελληνικά +Emiliàn e rumagnòl +Español +Esperanto +Estremeñu +Euskara +فارسی +Fiji Hindi +Føroyskt +Français +Frysk +Furlan +Gaeilge +Gaelg +Gagauz +Gàidhlig +Galego +贛語 +ગુજરાતી +客家語/Hak-kâ-ngî +Хальмг +한국어 +Hausa +Hawaii +Հայերեն +हिन्दी +Hornjoserbsce +Hrvatski +Ido +Igbo +Ilokano +বিষ্ণুপ্রিয়া মণিপুরী +Bahasa Indonesia +Interlingua +Interlingue +Ирон +IsiZulu +Íslenska +Italiano +עברית +Basa Jawa +Kalaallisut +ಕನ್ನಡ +Kapampangan +Къарачай-малкъар +ქართული +Kaszëbsczi +Қазақша +Kernowek +Kinyarwanda +Kiswahili +Коми +Kongo +Kreyòl ayisyen +Kurdî +Кыргызча +Кырык мары +Ladino +Лезги +ລາວ +Latgaļu +Latina +Latviešu +Lëtzebuergesch +Lietuvių +Ligure +Limburgs +Lingála +Lojban +Lumbaart +Magyar +Македонски +Malagasy +മലയാളം +Malti +Māori +मराठी +მარგალური +مصرى +مازِرونی +Bahasa Melayu +Mìng-dĕ̤ng-ngṳ̄ +Mirandés +Монгол +မြန်မာဘာသာ +Nāhuatl +Dorerin Naoero +Nederlands +Nedersaksies +नेपाली +नेपाल भाषा +日本語 +Napulitano +Нохчийн +Nordfriisk +Norfuk / Pitkern +Norsk bokmål +Norsk nynorsk +Nouormand +Novial +Occitan +Олык марий +ଓଡ଼ିଆ +Oromoo +Oʻzbekcha +ਪੰਜਾਬੀ +Pangasinan +پنجابی +Papiamentu +پښتو +Перем Коми +ភាសាខ្មែរ +Picard +Piemontèis +Tok Pisin +Plattdüütsch +Polski +Ποντιακά +Português +Qırımtatarca +Reo tahiti +Ripoarisch +Română +Romani +Rumantsch +Runa Simi +Русиньскый +Русский +Саха тыла +Sámegiella +संस्कृतम् +Sardu +Scots +Seeltersk +Shqip +Sicilianu +සිංහල +Simple English +SiSwati +Slovenčina +Slovenščina +Словѣньскъ / ⰔⰎⰑⰂⰡⰐⰠⰔⰍⰟ +Ślůnski +Soomaaliga +کوردی +Sranantongo +Српски / srpski +Srpskohrvatski / српскохрватски +Basa Sunda +Suomi +Svenska +Tagalog +தமிழ் +Taqbaylit +Tarandíne +Татарча/tatarça +తెలుగు +Tetun +ไทย +Тоҷикӣ +ᏣᎳᎩ +Tsetsêhestâhese +Türkçe +Twi +Удмурт +ᨅᨔ ᨕᨘᨁᨗ +Українська +اردو +ئۇيغۇرچە / Uyghurche +Vahcuengh +Vèneto +Vepsän kel’ +Tiếng Việt +Volapük +Võro +Walon +文言 +West-Vlams +Winaray +Wolof +吴语 +ייִדיש +Yorùbá +粵語 +Zazaki +Zeêuws +Žemaitėška +中文 +Edit links +This page was last modified on 22 November 2014 at 11:19. +Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc., a non-profit organization. +Privacy policyAbout WikipediaDisclaimersContact WikipediaDevelopersMobile viewWikimedia Foundation Powered by MediaWiki + + +World Trade Organization +From Wikipedia, the free encyclopedia +"WTO" redirects here. For other uses, see WTO (disambiguation). +World Trade Organization (English) +Organisation mondiale du commerce (French) +Organización Mundial del Comercio (Spanish) +World Trade Organization (logo and wordmark).svg +Official logo of WTO +WTO members and observers.svg + Members + Members, dually represented by the EU + Observers + Non-members +Abbreviation WTO +Formation 1 January 1995; 19 years ago +Type International trade organization +Purpose Liberalize international trade +Headquarters Centre William Rappard, Geneva, Switzerland +Coordinates 46.12°N 6.09°ECoordinates: 46.12°N 6.09°E +Region served Worldwide +Membership 160 member states[1] +Official language English, French, Spanish[2] +Director-General Roberto Azevêdo +Budget 196 million Swiss francs (approx. 209 million US$) in 2011.[3] +Staff 640[4] +Website www.wto.org +The World Trade Organization (WTO) is an organization that intends to supervise and liberalize international trade. The organization officially commenced on 1 January 1995 under the Marrakech Agreement, replacing the General Agreement on Tariffs and Trade (GATT), which commenced in 1948.[5] The organization deals with regulation of trade between participating countries by providing a framework for negotiating and formalizing trade agreements and a dispute resolution process aimed at enforcing participants' adherence to WTO agreements, which are signed by representatives of member governments[6]:fol.9–10 and ratified by their parliaments.[7] Most of the issues that the WTO focuses on derive from previous trade negotiations, especially from the Uruguay Round (1986–1994). +The organization is attempting to complete negotiations on the Doha Development Round, which was launched in 2001 with an explicit focus on addressing the needs of developing countries. As of June 2012, the future of the Doha Round remained uncertain: the work programme lists 21 subjects in which the original deadline of 1 January 2005 was missed, and the round is still incomplete.[8] The conflict between free trade on industrial goods and services but retention of protectionism on farm subsidies to domestic agricultural sector (requested by developed countries) and the substantiation of the international liberalization of fair trade on agricultural products (requested by developing countries) remain the major obstacles. These points of contention have hindered any progress to launch new WTO negotiations beyond the Doha Development Round. As a result of this impasse, there has been an increasing number of bilateral free trade agreements signed.[9] As of July 2012, there were various negotiation groups in the WTO system for the current agricultural trade negotiation which is in the condition of stalemate.[10] +WTO's current Director-General is Roberto Azevêdo,[11][12] who leads a staff of over 600 people in Geneva, Switzerland.[13] A trade facilitation agreement known as the Bali Package was reached by all members on 7 December 2013, the first comprehensive agreement in the organization's history.[14][15] +Contents [hide] +1 History +1.1 GATT rounds of negotiations +1.1.1 From Geneva to Tokyo +1.1.2 Uruguay Round +1.2 Ministerial conferences +1.3 Doha Round (Doha Agenda) +2 Functions +3 Principles of the trading system +4 Organizational structure +5 Decision-making +6 Dispute settlement +7 Accession and membership +7.1 Accession process +7.2 Members and observers +8 Agreements +9 Office of director-general +9.1 List of directors-general +10 See also +11 Notes and references +12 External links +History + +The economists Harry White (left) and John Maynard Keynes at the Bretton Woods Conference. Both had been strong advocates of a central-controlled international trade environment and recommended the establishment of three institutions: the IMF (for fiscal and monetary issues); the World Bank (for financial and structural issues); and the ITO (for international economic cooperation).[16] +The WTO's predecessor, the General Agreement on Tariffs and Trade (GATT), was established after World War II in the wake of other new multilateral institutions dedicated to international economic cooperation – notably the Bretton Woods institutions known as the World Bank and the International Monetary Fund. A comparable international institution for trade, named the International Trade Organization was successfully negotiated. The ITO was to be a United Nations specialized agency and would address not only trade barriers but other issues indirectly related to trade, including employment, investment, restrictive business practices, and commodity agreements. But the ITO treaty was not approved by the U.S. and a few other signatories and never went into effect.[17][18][19] +In the absence of an international organization for trade, the GATT would over the years "transform itself" into a de facto international organization.[20] +GATT rounds of negotiations +See also: General Agreement on Tariffs and Trade +The GATT was the only multilateral instrument governing international trade from 1946 until the WTO was established on 1 January 1995.[21] Despite attempts in the mid-1950s and 1960s to create some form of institutional mechanism for international trade, the GATT continued to operate for almost half a century as a semi-institutionalized multilateral treaty regime on a provisional basis.[22] +From Geneva to Tokyo +Seven rounds of negotiations occurred under GATT. The first real GATT trade rounds concentrated on further reducing tariffs. Then, the Kennedy Round in the mid-sixties brought about a GATT anti-dumping Agreement and a section on development. The Tokyo Round during the seventies was the first major attempt to tackle trade barriers that do not take the form of tariffs, and to improve the system, adopting a series of agreements on non-tariff barriers, which in some cases interpreted existing GATT rules, and in others broke entirely new ground. Because these plurilateral agreements were not accepted by the full GATT membership, they were often informally called "codes". Several of these codes were amended in the Uruguay Round, and turned into multilateral commitments accepted by all WTO members. Only four remained plurilateral (those on government procurement, bovine meat, civil aircraft and dairy products), but in 1997 WTO members agreed to terminate the bovine meat and dairy agreements, leaving only two.[21] +Uruguay Round +Main article: Uruguay Round + +During the Doha Round, the US government blamed Brazil and India for being inflexible and the EU for impeding agricultural imports.[23] The then-President of Brazil, Luiz Inácio Lula da Silva (above right), responded to the criticisms by arguing that progress would only be achieved if the richest countries (especially the US and countries in the EU) made deeper cuts in agricultural subsidies and further opened their markets for agricultural goods.[24] +Well before GATT's 40th anniversary, its members concluded that the GATT system was straining to adapt to a new globalizing world economy.[25][26] In response to the problems identified in the 1982 Ministerial Declaration (structural deficiencies, spill-over impacts of certain countries' policies on world trade GATT could not manage etc.), the eighth GATT round – known as the Uruguay Round – was launched in September 1986, in Punta del Este, Uruguay.[25] +It was the biggest negotiating mandate on trade ever agreed: the talks were going to extend the trading system into several new areas, notably trade in services and intellectual property, and to reform trade in the sensitive sectors of agriculture and textiles; all the original GATT articles were up for review.[26] The Final Act concluding the Uruguay Round and officially establishing the WTO regime was signed 15 April 1994, during the ministerial meeting at Marrakesh, Morocco, and hence is known as the Marrakesh Agreement.[27] +The GATT still exists as the WTO's umbrella treaty for trade in goods, updated as a result of the Uruguay Round negotiations (a distinction is made between GATT 1994, the updated parts of GATT, and GATT 1947, the original agreement which is still the heart of GATT 1994).[25] GATT 1994 is not however the only legally binding agreement included via the Final Act at Marrakesh; a long list of about 60 agreements, annexes, decisions and understandings was adopted. The agreements fall into a structure with six main parts: +The Agreement Establishing the WTO +Goods and investment – the Multilateral Agreements on Trade in Goods including the GATT 1994 and the Trade Related Investment Measures (TRIMS) +Services — the General Agreement on Trade in Services +Intellectual property – the Agreement on Trade-Related Aspects of Intellectual Property Rights (TRIPS) +Dispute settlement (DSU) +Reviews of governments' trade policies (TPRM)[28] +In terms of the WTO's principle relating to tariff "ceiling-binding" (No. 3), the Uruguay Round has been successful in increasing binding commitments by both developed and developing countries, as may be seen in the percentages of tariffs bound before and after the 1986–1994 talks.[29] +Ministerial conferences + +The World Trade Organization Ministerial Conference of 1998, in the Palace of Nations (Geneva, Switzerland). +The highest decision-making body of the WTO is the Ministerial Conference, which usually meets every two years. It brings together all members of the WTO, all of which are countries or customs unions. The Ministerial Conference can take decisions on all matters under any of the multilateral trade agreements. The inaugural ministerial conference was held in Singapore in 1996. Disagreements between largely developed and developing economies emerged during this conference over four issues initiated by this conference, which led to them being collectively referred to as the "Singapore issues". The second ministerial conference was held in Geneva in Switzerland. The third conference in Seattle, Washington ended in failure, with massive demonstrations and police and National Guard crowd-control efforts drawing worldwide attention. The fourth ministerial conference was held in Doha in the Persian Gulf nation of Qatar. The Doha Development Round was launched at the conference. The conference also approved the joining of China, which became the 143rd member to join. The fifth ministerial conference was held in Cancún, Mexico, aiming at forging agreement on the Doha round. An alliance of 22 southern states, the G20 developing nations (led by India, China,[30] Brazil, ASEAN led by the Philippines), resisted demands from the North for agreements on the so-called "Singapore issues" and called for an end to agricultural subsidies within the EU and the US. The talks broke down without progress. +The sixth WTO ministerial conference was held in Hong Kong from 13–18 December 2005. It was considered vital if the four-year-old Doha Development Round negotiations were to move forward sufficiently to conclude the round in 2006. In this meeting, countries agreed to phase out all their agricultural export subsidies by the end of 2013, and terminate any cotton export subsidies by the end of 2006. Further concessions to developing countries included an agreement to introduce duty-free, tariff-free access for goods from the Least Developed Countries, following the Everything but Arms initiative of the European Union — but with up to 3% of tariff lines exempted. Other major issues were left for further negotiation to be completed by the end of 2010. The WTO General Council, on 26 May 2009, agreed to hold a seventh WTO ministerial conference session in Geneva from 30 November-3 December 2009. A statement by chairman Amb. Mario Matus acknowledged that the prime purpose was to remedy a breach of protocol requiring two-yearly "regular" meetings, which had lapsed with the Doha Round failure in 2005, and that the "scaled-down" meeting would not be a negotiating session, but "emphasis will be on transparency and open discussion rather than on small group processes and informal negotiating structures". The general theme for discussion was "The WTO, the Multilateral Trading System and the Current Global Economic Environment"[31] +Doha Round (Doha Agenda) +Main article: Doha Development Round + +The Doha Development Round started in 2001 is at an impasse. +The WTO launched the current round of negotiations, the Doha Development Round, at the fourth ministerial conference in Doha, Qatar in November 2001. This was to be an ambitious effort to make globalization more inclusive and help the world's poor, particularly by slashing barriers and subsidies in farming.[32] The initial agenda comprised both further trade liberalization and new rule-making, underpinned by commitments to strengthen substantial assistance to developing countries.[33] +The negotiations have been highly contentious. Disagreements still continue over several key areas including agriculture subsidies, which emerged as critical in July 2006.[34] According to a European Union statement, "The 2008 Ministerial meeting broke down over a disagreement between exporters of agricultural bulk commodities and countries with large numbers of subsistence farmers on the precise terms of a 'special safeguard measure' to protect farmers from surges in imports."[35] The position of the European Commission is that "The successful conclusion of the Doha negotiations would confirm the central role of multilateral liberalisation and rule-making. It would confirm the WTO as a powerful shield against protectionist backsliding."[33] An impasse remains and, as of August 2013, agreement has not been reached, despite intense negotiations at several ministerial conferences and at other sessions. On 27 March 2013, the chairman of agriculture talks announced "a proposal to loosen price support disciplines for developing countries’ public stocks and domestic food aid." He added: “...we are not yet close to agreement—in fact, the substantive discussion of the proposal is only beginning.”[36] +[show]v · t · eGATT and WTO trade rounds[37] +Functions +Among the various functions of the WTO, these are regarded by analysts as the most important: +It oversees the implementation, administration and operation of the covered agreements.[38][39] +It provides a forum for negotiations and for settling disputes.[40][41] +Additionally, it is the WTO's duty to review and propagate the national trade policies, and to ensure the coherence and transparency of trade policies through surveillance in global economic policy-making.[39][41] Another priority of the WTO is the assistance of developing, least-developed and low-income countries in transition to adjust to WTO rules and disciplines through technical cooperation and training.[42] +(i) The WTO shall facilitate the implementation, administration and operation and further the objec­tives of this Agreement and of the Multilateral Trade Agreements, and shall also provide the frame work for the implementation, administration and operation of the multilateral Trade Agreements. +(ii) The WTO shall provide the forum for negotiations among its members concerning their multilateral trade relations in matters dealt with under the Agreement in the Annexes to this Agreement. +(iii) The WTO shall administer the Understanding on Rules and Procedures Governing the Settlement of Disputes. +(iv) The WTO shall administer Trade Policy Review Mechanism. +(v) With a view to achieving greater coherence in global economic policy making, the WTO shall cooperate, as appropriate, with the international Monetary Fund (IMF) and with the International Bank for Reconstruction and Development (IBRD) and its affiliated agencies. [43] +The above five listings are the additional functions of the World Trade Organization. As globalization proceeds in today's society, the necessity of an International Organization to manage the trading systems has been of vital importance. As the trade volume increases, issues such as protectionism, trade barriers, subsidies, violation of intellectual property arise due to the differences in the trading rules of every nation. The World Trade Organization serves as the mediator between the nations when such problems arise. WTO could be referred to as the product of globalization and also as one of the most important organizations in today's globalized society. +The WTO is also a center of economic research and analysis: regular assessments of the global trade picture in its annual publications and research reports on specific topics are produced by the organization.[44] Finally, the WTO cooperates closely with the two other components of the Bretton Woods system, the IMF and the World Bank.[40] +Principles of the trading system +The WTO establishes a framework for trade policies; it does not define or specify outcomes. That is, it is concerned with setting the rules of the trade policy games.[45] Five principles are of particular importance in understanding both the pre-1994 GATT and the WTO: +Non-discrimination. It has two major components: the most favoured nation (MFN) rule, and the national treatment policy. Both are embedded in the main WTO rules on goods, services, and intellectual property, but their precise scope and nature differ across these areas. The MFN rule requires that a WTO member must apply the same conditions on all trade with other WTO members, i.e. a WTO member has to grant the most favorable conditions under which it allows trade in a certain product type to all other WTO members.[45] "Grant someone a special favour and you have to do the same for all other WTO members."[29] National treatment means that imported goods should be treated no less favorably than domestically produced goods (at least after the foreign goods have entered the market) and was introduced to tackle non-tariff barriers to trade (e.g. technical standards, security standards et al. discriminating against imported goods).[45] +Reciprocity. It reflects both a desire to limit the scope of free-riding that may arise because of the MFN rule, and a desire to obtain better access to foreign markets. A related point is that for a nation to negotiate, it is necessary that the gain from doing so be greater than the gain available from unilateral liberalization; reciprocal concessions intend to ensure that such gains will materialise.[46] +Binding and enforceable commitments. The tariff commitments made by WTO members in a multilateral trade negotiation and on accession are enumerated in a schedule (list) of concessions. These schedules establish "ceiling bindings": a country can change its bindings, but only after negotiating with its trading partners, which could mean compensating them for loss of trade. If satisfaction is not obtained, the complaining country may invoke the WTO dispute settlement procedures.[29][46] +Transparency. The WTO members are required to publish their trade regulations, to maintain institutions allowing for the review of administrative decisions affecting trade, to respond to requests for information by other members, and to notify changes in trade policies to the WTO. These internal transparency requirements are supplemented and facilitated by periodic country-specific reports (trade policy reviews) through the Trade Policy Review Mechanism (TPRM).[47] The WTO system tries also to improve predictability and stability, discouraging the use of quotas and other measures used to set limits on quantities of imports.[29] +Safety valves. In specific circumstances, governments are able to restrict trade. The WTO's agreements permit members to take measures to protect not only the environment but also public health, animal health and plant health.[48] +There are three types of provision in this direction: +articles allowing for the use of trade measures to attain non-economic objectives; +articles aimed at ensuring "fair competition"; members must not use environmental protection measures as a means of disguising protectionist policies.[48] +provisions permitting intervention in trade for economic reasons.[47] +Exceptions to the MFN principle also allow for preferential treatment of developing countries, regional free trade areas and customs unions.[6]:fol.93 +Organizational structure +The General Council has the following subsidiary bodies which oversee committees in different areas: +Council for Trade in Goods +There are 11 committees under the jurisdiction of the Goods Council each with a specific task. All members of the WTO participate in the committees. The Textiles Monitoring Body is separate from the other committees but still under the jurisdiction of Goods Council. The body has its own chairman and only 10 members. The body also has several groups relating to textiles.[49] +Council for Trade-Related Aspects of Intellectual Property Rights +Information on intellectual property in the WTO, news and official records of the activities of the TRIPS Council, and details of the WTO's work with other international organizations in the field.[50] +Council for Trade in Services +The Council for Trade in Services operates under the guidance of the General Council and is responsible for overseeing the functioning of the General Agreement on Trade in Services (GATS). It is open to all WTO members, and can create subsidiary bodies as required.[51] +Trade Negotiations Committee +The Trade Negotiations Committee (TNC) is the committee that deals with the current trade talks round. The chair is WTO's director-general. As of June 2012 the committee was tasked with the Doha Development Round.[52] +The Service Council has three subsidiary bodies: financial services, domestic regulations, GATS rules and specific commitments.[49] The council has several different committees, working groups, and working parties.[53] There are committees on the following: Trade and Environment; Trade and Development (Subcommittee on Least-Developed Countries); Regional Trade Agreements; Balance of Payments Restrictions; and Budget, Finance and Administration. There are working parties on the following: Accession. There are working groups on the following: Trade, debt and finance; and Trade and technology transfer. +Decision-making +The WTO describes itself as "a rules-based, member-driven organization — all decisions are made by the member governments, and the rules are the outcome of negotiations among members".[54] The WTO Agreement foresees votes where consensus cannot be reached, but the practice of consensus dominates the process of decision-making.[55] +Richard Harold Steinberg (2002) argues that although the WTO's consensus governance model provides law-based initial bargaining, trading rounds close through power-based bargaining favouring Europe and the U.S., and may not lead to Pareto improvement.[56] +Dispute settlement +Main article: Dispute settlement in the WTO +In 1994, the WTO members agreed on the Understanding on Rules and Procedures Governing the Settlement of Disputes (DSU) annexed to the "Final Act" signed in Marrakesh in 1994.[57] Dispute settlement is regarded by the WTO as the central pillar of the multilateral trading system, and as a "unique contribution to the stability of the global economy".[58] WTO members have agreed that, if they believe fellow-members are violating trade rules, they will use the multilateral system of settling disputes instead of taking action unilaterally.[59] +The operation of the WTO dispute settlement process involves the DSB panels, the Appellate Body, the WTO Secretariat, arbitrators, independent experts and several specialized institutions.[60] Bodies involved in the dispute settlement process, World Trade Organization. +Accession and membership +Main article: World Trade Organization accession and membership +The process of becoming a WTO member is unique to each applicant country, and the terms of accession are dependent upon the country's stage of economic development and current trade regime.[61] The process takes about five years, on average, but it can last more if the country is less than fully committed to the process or if political issues interfere. The shortest accession negotiation was that of the Kyrgyz Republic, while the longest was that of Russia, which, having first applied to join GATT in 1993, was approved for membership in December 2011 and became a WTO member on 22 August 2012.[62] The second longest was that of Vanuatu, whose Working Party on the Accession of Vanuatu was established on 11 July 1995. After a final meeting of the Working Party in October 2001, Vanuatu requested more time to consider its accession terms. In 2008, it indicated its interest to resume and conclude its WTO accession. The Working Party on the Accession of Vanuatu was reconvened informally on 4 April 2011 to discuss Vanuatu's future WTO membership. The re-convened Working Party completed its mandate on 2 May 2011. The General Council formally approved the Accession Package of Vanuatu on 26 October 2011. On 24 August 2012, the WTO welcomed Vanuatu as its 157th member.[63] An offer of accession is only given once consensus is reached among interested parties.[64] +Accession process + +WTO accession progress: + Members (including dual-representation with the European Union) + Draft Working Party Report or Factual Summary adopted + Goods and/or Services offers submitted + Memorandum on Foreign Trade Regime (FTR) submitted + Observer, negotiations to start later or no Memorandum on FTR submitted + Frozen procedures or no negotiations in the last 3 years + No official interaction with the WTO +A country wishing to accede to the WTO submits an application to the General Council, and has to describe all aspects of its trade and economic policies that have a bearing on WTO agreements.[65] The application is submitted to the WTO in a memorandum which is examined by a working party open to all interested WTO Members.[66] +After all necessary background information has been acquired, the working party focuses on issues of discrepancy between the WTO rules and the applicant's international and domestic trade policies and laws. The working party determines the terms and conditions of entry into the WTO for the applicant nation, and may consider transitional periods to allow countries some leeway in complying with the WTO rules.[61] +The final phase of accession involves bilateral negotiations between the applicant nation and other working party members regarding the concessions and commitments on tariff levels and market access for goods and services. The new member's commitments are to apply equally to all WTO members under normal non-discrimination rules, even though they are negotiated bilaterally.[65] +When the bilateral talks conclude, the working party sends to the general council or ministerial conference an accession package, which includes a summary of all the working party meetings, the Protocol of Accession (a draft membership treaty), and lists ("schedules") of the member-to-be's commitments. Once the general council or ministerial conference approves of the terms of accession, the applicant's parliament must ratify the Protocol of Accession before it can become a member.[67] Some countries may have faced tougher and a much longer accession process due to challenges during negotiations with other WTO members, such as Vietnam, whose negotiations took more than 11 years before it became official member in January 2007.[68] +Members and observers +The WTO has 160 members and 24 observer governments.[69] In addition to states, the European Union is a member. WTO members do not have to be full sovereign nation-members. Instead, they must be a customs territory with full autonomy in the conduct of their external commercial relations. Thus Hong Kong has been a member since 1995 (as "Hong Kong, China" since 1997) predating the People's Republic of China, which joined in 2001 after 15 years of negotiations. The Republic of China (Taiwan) acceded to the WTO in 2002 as "Separate Customs Territory of Taiwan, Penghu, Kinmen and Matsu" (Chinese Taipei) despite its disputed status.[70] The WTO Secretariat omits the official titles (such as Counselor, First Secretary, Second Secretary and Third Secretary) of the members of Chinese Taipei's Permanent Mission to the WTO, except for the titles of the Permanent Representative and the Deputy Permanent Representative.[71] +As of 2007, WTO member states represented 96.4% of global trade and 96.7% of global GDP.[72] Iran, followed by Algeria, are the economies with the largest GDP and trade outside the WTO, using 2005 data.[73][74] With the exception of the Holy See, observers must start accession negotiations within five years of becoming observers. A number of international intergovernmental organizations have also been granted observer status to WTO bodies.[75] 14 UN member states have no official affiliation with the WTO. +Agreements +Further information: Uruguay Round +The WTO oversees about 60 different agreements which have the status of international legal texts. Member countries must sign and ratify all WTO agreements on accession.[76] A discussion of some of the most important agreements follows. The Agreement on Agriculture came into effect with the establishment of the WTO at the beginning of 1995. The AoA has three central concepts, or "pillars": domestic support, market access and export subsidies. The General Agreement on Trade in Services was created to extend the multilateral trading system to service sector, in the same way as the General Agreement on Tariffs and Trade (GATT) provided such a system for merchandise trade. The agreement entered into force in January 1995. The Agreement on Trade-Related Aspects of Intellectual Property Rights sets down minimum standards for many forms of intellectual property (IP) regulation. It was negotiated at the end of the Uruguay Round of the General Agreement on Tariffs and Trade (GATT) in 1994.[77] +The Agreement on the Application of Sanitary and Phytosanitary Measures—also known as the SPS Agreement—was negotiated during the Uruguay Round of GATT, and entered into force with the establishment of the WTO at the beginning of 1995. Under the SPS agreement, the WTO sets constraints on members' policies relating to food safety (bacterial contaminants, pesticides, inspection and labelling) as well as animal and plant health (imported pests and diseases). The Agreement on Technical Barriers to Trade is an international treaty of the World Trade Organization. It was negotiated during the Uruguay Round of the General Agreement on Tariffs and Trade, and entered into force with the establishment of the WTO at the end of 1994. The object ensures that technical negotiations and standards, as well as testing and certification procedures, do not create unnecessary obstacles to trade".[78] The Agreement on Customs Valuation, formally known as the Agreement on Implementation of Article VII of GATT, prescribes methods of customs valuation that Members are to follow. Chiefly, it adopts the "transaction value" approach. +In December 2013, the biggest agreement within the WTO was signed and known as the Bali Package.[79] +Office of director-general + +The headquarters of the World Trade Organization, in Geneva, Switzerland. +The procedures for the appointment of the WTO director-general were published in January 2003.[80] Additionally, there are four deputy directors-general. As of 1 October 2013, under director-general Roberto Azevêdo, the four deputy directors-general are Yi Xiaozhun of China, Karl-Ernst Brauner of Germany, Yonov Frederick Agah of Nigeria and David Shark of the United States.[81] +List of directors-general +Source: Official website[82] +Brazil Roberto Azevedo, 2013– +France Pascal Lamy, 2005–2013 +Thailand Supachai Panitchpakdi, 2002–2005 +New Zealand Mike Moore, 1999–2002 +Italy Renato Ruggiero, 1995–1999 +Republic of Ireland Peter Sutherland, 1995 +(Heads of the precursor organization, GATT): +Republic of Ireland Peter Sutherland, 1993–1995 +Switzerland Arthur Dunkel, 1980–1993 +Switzerland Olivier Long, 1968–1980 +United Kingdom Eric Wyndham White, 1948–1968 +See also +Agreement on Trade Related Investment Measures (TRIMS) +Agreement on Trade-Related Aspects of Intellectual Property Rights (TRIPS) +Aide-mémoire non-paper +Anti-globalization movement +Criticism of the World Trade Organization +Foreign Affiliate Trade Statistics +Global administrative law +Globality +Information Technology Agreement +International Trade Centre +Labour Standards in the World Trade Organisation +List of member states of the World Trade Organization +North American Free Trade Agreement (NAFTA) +Subsidy +Swiss Formula +Trade bloc +Washington Consensus +World Trade Report +World Trade Organization Ministerial Conference of 1999 protest activity +China and the World Trade Organization +Notes and references +Jump up ^ Members and Observers at WTO official website +Jump up ^ Languages, Documentation and Information Management Division at WTO official site +Jump up ^ "WTO Secretariat budget for 2011". WTO official site. Retrieved 25 August 2008. +Jump up ^ Understanding the WTO: What We Stand For_ Fact File +Jump up ^ World Trade Organization - UNDERSTANDING THE WTO: BASICS +^ Jump up to: a b Understanding the WTO Handbook at WTO official website. (Note that the document's printed folio numbers do not match the pdf page numbers.) +Jump up ^ Malanczuk, P. (1999). "International Organisations and Space Law: World Trade Organization". Encyclopaedia Britannica 442. p. 305. Bibcode:1999ESASP.442..305M. +Jump up ^ Understanding the WTO: The Doha Agenda +Jump up ^ The Challenges to the World Trade Organization: It’s All About Legitimacy THE BROOKINGS INSTITUTION, Policy Paper 2011-04 +Jump up ^ GROUPS IN THE WTO Updated 1 July 2013 +Jump up ^ Bourcier, Nicolas (21 May 2013). "Roberto Azevedo's WTO appointment gives Brazil a seat at the top table". Guardian Weekly. Retrieved 2 September 2013. +Jump up ^ "Roberto Azevêdo takes over". WTO official website. 1 September 2013. Retrieved 2 September 2013. +Jump up ^ "Overview of the WTO Secretariat". WTO official website. Retrieved 2 September 2013. +Jump up ^ Ninth WTO Ministerial Conference | WTO - MC9 +Jump up ^ BBC News - WTO agrees global trade deal worth $1tn +Jump up ^ A.E. Eckes Jr., US Trade History, 73 +* A. Smithies, Reflections on the Work of Keynes, 578–601 +* N. Warren, Internet and Globalization, 193 +Jump up ^ P. van den Bossche, The Law and Policy of the World Trade Organization, 80 +Jump up ^ Palmeter-Mavroidis, Dispute Settlement, 2 +Jump up ^ Fergusson, Ian F. (9 May 2007). "The World Trade Organization: Background and Issues" (PDF). Congressional Research Service. p. 4. Retrieved 15 August 2008. +Jump up ^ It was contemplated that the GATT would be applied for several years until the ITO came into force. However, since the ITO was never brought into being, the GATT gradually became the focus for international governmental cooperation on trade matters with economist Nicholas Halford overseeing the implementation of GATT in members policies. (P. van den Bossche, The Law and Policy of the World Trade Organization, 81; J.H. Jackson, Managing the Trading System, 134). +^ Jump up to: a b The GATT Years: from Havana to Marrakesh, WTO official site +Jump up ^ Footer, M. E. Analysis of the World Trade Organization, 17 +Jump up ^ B.S. Klapper, With a "Short Window" +Jump up ^ Lula, Time to Get Serious about Agricultural Subsidies +^ Jump up to: a b c P. Gallagher, The First Ten Years of the WTO, 4 +^ Jump up to: a b The Uruguay Round, WTO official site +Jump up ^ "Legal texts – Marrakesh agreement". WTO. Retrieved 30 May 2010. +Jump up ^ Overview: a Navigational Guide, WTO official site. For the complete list of "The Uruguay Round Agreements", see WTO legal texts, WTO official site, and Uruguay Round Agreements, Understandings, Decisions and Declarations, WorldTradeLaw.net +^ Jump up to: a b c d Principles of the Trading System, WTO official site +Jump up ^ "Five Years of China WTO Membership. EU and US Perspectives about China's Compliance with Transparency Commitments and the Transitional Review Mechanism". Papers.ssrn.com. Retrieved 30 May 2010. +Jump up ^ WTO to hold 7th Ministerial Conference on 30 November-2 December 2009 WTO official website +Jump up ^ "In the twilight of Doha". The Economist (The Economist): 65. 27 July 2006. +^ Jump up to: a b European Commission The Doha Round +Jump up ^ Fergusson, Ian F. (18 January 2008). "World Trade Organization Negotiations: The Doha Development Agenda" (PDF). Congressional Research Service. Retrieved 13 April 2012. Page 9 (folio CRS-6) +Jump up ^ WTO trade negotiations: Doha Development Agenda Europa press release, 31 October 2011 +Jump up ^ "Members start negotiating proposal on poor countries’ food stockholding". WTO official website. 27 March 2013. Retrieved 2 September 2013. +Jump up ^ a)The GATT years: from Havana to Marrakesh, World Trade Organization +b)Timeline: World Trade Organization – A chronology of key events, BBC News +c)Brakman-Garretsen-Marrewijk-Witteloostuijn, Nations and Firms in the Global Economy, Chapter 10: Trade and Capital Restriction +Jump up ^ Functions of the WTO, IISD +^ Jump up to: a b Main Functions, WTO official site +^ Jump up to: a b A Bredimas, International Economic Law, II, 17 +^ Jump up to: a b C. Deere, Decision-making in the WTO: Medieval or Up-to-Date? +Jump up ^ WTO Assistance for Developing Countries[dead link], WTO official site +Jump up ^ Sinha, Aparijita. [1]. "What are the functions and objectives of the WTO?". Retrieved on 13 April, 2014. +Jump up ^ Economic research and analysis, WTO official site +^ Jump up to: a b c B. Hoekman, The WTO: Functions and Basic Principles, 42 +^ Jump up to: a b B. Hoekman, The WTO: Functions and Basic Principles, 43 +^ Jump up to: a b B. Hoekman, The WTO: Functions and Basic Principles, 44 +^ Jump up to: a b Understanding the WTO: What we stand for +^ Jump up to: a b "Fourth level: down to the nitty-gritty". WTO official site. Retrieved 18 August 2008. +Jump up ^ "Intellectual property – overview of TRIPS Agreement". Wto.org. 15 April 1994. Retrieved 30 May 2010. +Jump up ^ "The Services Council, its Committees and other subsidiary bodies". WTO official site. Retrieved 14 August 2008. +Jump up ^ "The Trade Negotiations Committee". WTO official site. Retrieved 14 August 2008. +Jump up ^ "WTO organization chart". WTO official site. Retrieved 14 August 2008. +Jump up ^ Decision-making at WTO official site +Jump up ^ Decision-Making in the World Trade Organization Abstract from Journal of International Economic Law at Oxford Journals +Jump up ^ Steinberg, Richard H. "In the Shadow of Law or Power? Consensus-based Bargaining and Outcomes in the GATT/WTO." International Organization. Spring 2002. pp. 339–374. +Jump up ^ Stewart-Dawyer, The WTO Dispute Settlement System, 7 +Jump up ^ S. Panitchpakdi, The WTO at ten, 8. +Jump up ^ Settling Disputes:a Unique Contribution, WTO official site +Jump up ^ "Disputes – Dispute Settlement CBT – WTO Bodies involved in the dispute settlement process – The Dispute Settlement Body (DSB) – Page 1". WTO. 25 July 1996. Retrieved 21 May 2011. +^ Jump up to: a b Accessions Summary, Center for International Development +Jump up ^ Ministerial Conference approves Russia's WTO membership WTO News Item, 16 December 2011 +Jump up ^ Accession status: Vanuatu. WTO. Retrieved on 12 July 2013. +Jump up ^ C. Michalopoulos, WTO Accession, 64 +^ Jump up to: a b Membership, Alliances and Bureaucracy, WTO official site +Jump up ^ C. Michalopoulos, WTO Accession, 62–63 +Jump up ^ How to Become a Member of the WTO, WTO official site +Jump up ^ Napier, Nancy K.; Vuong, Quan Hoang (2013). What we see, why we worry, why we hope: Vietnam going forward. Boise, ID, USA: Boise State University CCI Press. p. 140. ISBN 978-0985530587. +Jump up ^ "Members and Observers". World Trade Organization. 24 August 2012. +Jump up ^ Jackson, J. H. Sovereignty, 109 +Jump up ^ ROC Government Publication +Jump up ^ "Accession in perspective". World Trade Organization. Retrieved 22 December 2013. +Jump up ^ "ANNEX 1. STATISTICAL SURVEY". World Trade Organization. 2005. Retrieved 22 December 2013. +Jump up ^ Arjomandy, Danial (21 November 2013). "Iranian Membership in the World Trade Organization: An Unclear Future". Iranian Studies. Retrieved 22 December 2013. +Jump up ^ International intergovernmental organizations granted observer status to WTO bodies at WTO official website +Jump up ^ "Legal texts – the WTO agreements". WTO. Retrieved 30 May 2010. +Jump up ^ Understanding the WTO - Intellectual property: protection and enforcement. WTO. Retrieved on 29 July 2013. +Jump up ^ "A Summary of the Final Act of the Uruguay Round". Wto.org. Retrieved 30 May 2010. +Jump up ^ Zarocostas, John (7 December 2013). "Global Trade Deal Reached". WWD. Retrieved 8 December 2013. +Jump up ^ "WT/L/509". WTO. Retrieved 18 February 2013. +Jump up ^ "Director-General Elect Azevêdo announces his four Deputy Directors-General". 17 August 2013. Retrieved 2 September 2013. +Jump up ^ "Previous GATT and WTO Directors-General". WTO. Retrieved 21 May 2011. +External links + Wikiquote has quotations related to: World Trade Organization + Wikimedia Commons has media related to World Trade Organization. +Official pages +Official WTO homepage +WTO 10th Anniversary PDF (1.40 MB) — Highlights of the first decade, Annual Report 2005 pages 116–166 +Glossary of terms—a guide to 'WTO-speak' +International Trade Centre — joint UN/WTO agency +Government pages on the WTO +European Union position on the WTO +Media pages on the WTO +World Trade Organization +BBC News — Profile: WTO +Guardian Unlimited — Special Report: The World Trade Organisation ongoing coverage +Non-governmental organization pages on the WTO +Gatt.org — Parody of official WTO page by The Yes Men +Public Citizen +Transnational Institute: Beyond the WTO +[show] v t e +World Trade Organization +[show] v t e +International trade +[show] v t e +International organizations +Authority control +WorldCat VIAF: 149937768 LCCN: no94018277 ISNI: 0000 0001 2296 2735 GND: 2145784-0 SELIBR: 135910 ULAN: 500292980 NDL: 00577475 NKC: kn20010711437 BNE: XX4574846 +Categories: World Trade OrganizationInternational tradeInternational trade organizationsOrganisations based in GenevaOrganizations established in 1995World government +Navigation menu +Create accountLog inArticleTalkReadView sourceView history + +Main page +Contents +Featured content +Current events +Random article +Donate to Wikipedia +Wikimedia Shop +Interaction +Help +About Wikipedia +Community portal +Recent changes +Contact page +Tools +What links here +Related changes +Upload file +Special pages +Permanent link +Page information +Wikidata item +Cite this page +Print/export +Create a book +Download as PDF +Printable version +Languages +Afrikaans +العربية +Aragonés +Asturianu +Azərbaycanca +বাংলা +Bân-lâm-gú +Беларуская +Беларуская (тарашкевіца)‎ +Български +Bosanski +Brezhoneg +Català +Čeština +Cymraeg +Dansk +Deutsch +Eesti +Ελληνικά +Español +Esperanto +Euskara +فارسی +Fiji Hindi +Føroyskt +Français +Frysk +Galego +ગુજરાતી +客家語/Hak-kâ-ngî +한국어 +Հայերեն +हिन्दी +Hrvatski +Ido +Ilokano +Bahasa Indonesia +Íslenska +Italiano +עברית +Basa Jawa +ಕನ್ನಡ +Къарачай-малкъар +ქართული +Қазақша +Kiswahili +Latina +Latviešu +Lietuvių +Magyar +Македонски +മലയാളം +मराठी +مصرى +Bahasa Melayu +Baso Minangkabau +မြန်မာဘာသာ +Nederlands +नेपाली +नेपाल भाषा +日本語 +Нохчийн +Norsk bokmål +Norsk nynorsk +Occitan +Oʻzbekcha +ਪੰਜਾਬੀ +پنجابی +پښتو +ភាសាខ្មែរ +Piemontèis +Polski +Português +Română +Русиньскый +Русский +Саха тыла +Shqip +සිංහල +Simple English +Slovenčina +Slovenščina +کوردی +Српски / srpski +Srpskohrvatski / српскохрватски +Suomi +Svenska +Tagalog +தமிழ் +Татарча/tatarça +తెలుగు +ไทย +Тоҷикӣ +Türkçe +Türkmençe +Українська +اردو +ئۇيغۇرچە / Uyghurche +Tiếng Việt +Winaray +ייִדיש +Yorùbá +粵語 +Žemaitėška +中文 +Edit links +This page was last modified on 22 November 2014 at 14:33. +Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By using this site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the Wikimedia Foundation, Inc., a non-profit organization. +Privacy policyAbout WikipediaDisclaimersContact WikipediaDevelopersMobile viewWikimedia Foundation Powered by MediaWiki \ No newline at end of file diff --git a/search/facet/facet_builder_datetime.go b/search/facet/facet_builder_datetime.go new file mode 100644 index 0000000..9fe4cf4 --- /dev/null +++ b/search/facet/facet_builder_datetime.go @@ -0,0 +1,163 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package facet + +import ( + "reflect" + "sort" + "time" + + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" +) + +var ( + reflectStaticSizeDateTimeFacetBuilder int + reflectStaticSizedateTimeRange int +) + +func init() { + var dtfb DateTimeFacetBuilder + reflectStaticSizeDateTimeFacetBuilder = int(reflect.TypeOf(dtfb).Size()) + var dtr dateTimeRange + reflectStaticSizedateTimeRange = int(reflect.TypeOf(dtr).Size()) +} + +type dateTimeRange struct { + start time.Time + end time.Time +} + +type DateTimeFacetBuilder struct { + size int + field string + termsCount map[string]int + total int + missing int + ranges map[string]*dateTimeRange + sawValue bool +} + +func NewDateTimeFacetBuilder(field string, size int) *DateTimeFacetBuilder { + return &DateTimeFacetBuilder{ + size: size, + field: field, + termsCount: make(map[string]int), + ranges: make(map[string]*dateTimeRange, 0), + } +} + +func (fb *DateTimeFacetBuilder) Size() int { + sizeInBytes := reflectStaticSizeDateTimeFacetBuilder + size.SizeOfPtr + + len(fb.field) + + for k := range fb.termsCount { + sizeInBytes += size.SizeOfString + len(k) + + size.SizeOfInt + } + + for k := range fb.ranges { + sizeInBytes += size.SizeOfString + len(k) + + size.SizeOfPtr + reflectStaticSizedateTimeRange + } + + return sizeInBytes +} + +func (fb *DateTimeFacetBuilder) AddRange(name string, start, end time.Time) { + r := dateTimeRange{ + start: start, + end: end, + } + fb.ranges[name] = &r +} + +func (fb *DateTimeFacetBuilder) Field() string { + return fb.field +} + +func (fb *DateTimeFacetBuilder) UpdateVisitor(term []byte) { + fb.sawValue = true + // only consider the values which are shifted 0 + prefixCoded := numeric.PrefixCoded(term) + shift, err := prefixCoded.Shift() + if err == nil && shift == 0 { + i64, err := prefixCoded.Int64() + if err == nil { + t := time.Unix(0, i64) + + // look at each of the ranges for a match + for rangeName, r := range fb.ranges { + if (r.start.IsZero() || t.After(r.start) || t.Equal(r.start)) && (r.end.IsZero() || t.Before(r.end)) { + fb.termsCount[rangeName] = fb.termsCount[rangeName] + 1 + fb.total++ + } + } + } + } +} + +func (fb *DateTimeFacetBuilder) StartDoc() { + fb.sawValue = false +} + +func (fb *DateTimeFacetBuilder) EndDoc() { + if !fb.sawValue { + fb.missing++ + } +} + +func (fb *DateTimeFacetBuilder) Result() *search.FacetResult { + rv := search.FacetResult{ + Field: fb.field, + Total: fb.total, + Missing: fb.missing, + } + + rv.DateRanges = make([]*search.DateRangeFacet, 0, len(fb.termsCount)) + + for term, count := range fb.termsCount { + dateRange := fb.ranges[term] + tf := &search.DateRangeFacet{ + Name: term, + Count: count, + } + if !dateRange.start.IsZero() { + start := dateRange.start.Format(time.RFC3339Nano) + tf.Start = &start + } + if !dateRange.end.IsZero() { + end := dateRange.end.Format(time.RFC3339Nano) + tf.End = &end + } + rv.DateRanges = append(rv.DateRanges, tf) + } + + sort.Sort(rv.DateRanges) + + // we now have the list of the top N facets + if fb.size < len(rv.DateRanges) { + rv.DateRanges = rv.DateRanges[:fb.size] + } + + notOther := 0 + for _, nr := range rv.DateRanges { + notOther += nr.Count + } + rv.Other = fb.total - notOther + + return &rv +} diff --git a/search/facet/facet_builder_numeric.go b/search/facet/facet_builder_numeric.go new file mode 100644 index 0000000..1383942 --- /dev/null +++ b/search/facet/facet_builder_numeric.go @@ -0,0 +1,157 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package facet + +import ( + "reflect" + "sort" + + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" +) + +var ( + reflectStaticSizeNumericFacetBuilder int + reflectStaticSizenumericRange int +) + +func init() { + var nfb NumericFacetBuilder + reflectStaticSizeNumericFacetBuilder = int(reflect.TypeOf(nfb).Size()) + var nr numericRange + reflectStaticSizenumericRange = int(reflect.TypeOf(nr).Size()) +} + +type numericRange struct { + min *float64 + max *float64 +} + +type NumericFacetBuilder struct { + size int + field string + termsCount map[string]int + total int + missing int + ranges map[string]*numericRange + sawValue bool +} + +func NewNumericFacetBuilder(field string, size int) *NumericFacetBuilder { + return &NumericFacetBuilder{ + size: size, + field: field, + termsCount: make(map[string]int), + ranges: make(map[string]*numericRange, 0), + } +} + +func (fb *NumericFacetBuilder) Size() int { + sizeInBytes := reflectStaticSizeNumericFacetBuilder + size.SizeOfPtr + + len(fb.field) + + for k := range fb.termsCount { + sizeInBytes += size.SizeOfString + len(k) + + size.SizeOfInt + } + + for k := range fb.ranges { + sizeInBytes += size.SizeOfString + len(k) + + size.SizeOfPtr + reflectStaticSizenumericRange + } + + return sizeInBytes +} + +func (fb *NumericFacetBuilder) AddRange(name string, min, max *float64) { + r := numericRange{ + min: min, + max: max, + } + fb.ranges[name] = &r +} + +func (fb *NumericFacetBuilder) Field() string { + return fb.field +} + +func (fb *NumericFacetBuilder) UpdateVisitor(term []byte) { + fb.sawValue = true + // only consider the values which are shifted 0 + prefixCoded := numeric.PrefixCoded(term) + shift, err := prefixCoded.Shift() + if err == nil && shift == 0 { + i64, err := prefixCoded.Int64() + if err == nil { + f64 := numeric.Int64ToFloat64(i64) + + // look at each of the ranges for a match + for rangeName, r := range fb.ranges { + if (r.min == nil || f64 >= *r.min) && (r.max == nil || f64 < *r.max) { + fb.termsCount[rangeName] = fb.termsCount[rangeName] + 1 + fb.total++ + } + } + } + } +} + +func (fb *NumericFacetBuilder) StartDoc() { + fb.sawValue = false +} + +func (fb *NumericFacetBuilder) EndDoc() { + if !fb.sawValue { + fb.missing++ + } +} + +func (fb *NumericFacetBuilder) Result() *search.FacetResult { + rv := search.FacetResult{ + Field: fb.field, + Total: fb.total, + Missing: fb.missing, + } + + rv.NumericRanges = make([]*search.NumericRangeFacet, 0, len(fb.termsCount)) + + for term, count := range fb.termsCount { + numericRange := fb.ranges[term] + tf := &search.NumericRangeFacet{ + Name: term, + Count: count, + Min: numericRange.min, + Max: numericRange.max, + } + + rv.NumericRanges = append(rv.NumericRanges, tf) + } + + sort.Sort(rv.NumericRanges) + + // we now have the list of the top N facets + if fb.size < len(rv.NumericRanges) { + rv.NumericRanges = rv.NumericRanges[:fb.size] + } + + notOther := 0 + for _, nr := range rv.NumericRanges { + notOther += nr.Count + } + rv.Other = fb.total - notOther + + return &rv +} diff --git a/search/facet/facet_builder_numeric_test.go b/search/facet/facet_builder_numeric_test.go new file mode 100644 index 0000000..dea2e04 --- /dev/null +++ b/search/facet/facet_builder_numeric_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package facet + +import ( + "strconv" + "testing" + + "github.com/blevesearch/bleve/v2/numeric" +) + +var pcodedvalues []numeric.PrefixCoded + +func init() { + pcodedvalues = []numeric.PrefixCoded{{0x20, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, {0x20, 0x0, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f}, {0x20, 0x0, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7a, 0x1d, 0xa}, {0x20, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, 0x16, 0x9, 0x4a, 0x7b}} +} + +func BenchmarkNumericFacet10(b *testing.B) { + numericFacetN(b, 10) +} + +func BenchmarkNumericFacet100(b *testing.B) { + numericFacetN(b, 100) +} + +func BenchmarkNumericFacet1000(b *testing.B) { + numericFacetN(b, 1000) +} + +func numericFacetN(b *testing.B, numTerms int) { + field := "test" + nfb := NewNumericFacetBuilder(field, numTerms) + min, max := 0.0, 9999999998.0 + + for i := 0; i <= numTerms; i++ { + max++ + min-- + + nfb.AddRange("rangename"+strconv.Itoa(i), &min, &max) + + for _, pv := range pcodedvalues { + nfb.StartDoc() + nfb.UpdateVisitor(pv) + nfb.EndDoc() + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + nfb.Result() + } +} diff --git a/search/facet/facet_builder_terms.go b/search/facet/facet_builder_terms.go new file mode 100644 index 0000000..ad1825c --- /dev/null +++ b/search/facet/facet_builder_terms.go @@ -0,0 +1,115 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package facet + +import ( + "reflect" + "sort" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" +) + +var reflectStaticSizeTermsFacetBuilder int + +func init() { + var tfb TermsFacetBuilder + reflectStaticSizeTermsFacetBuilder = int(reflect.TypeOf(tfb).Size()) +} + +type TermsFacetBuilder struct { + size int + field string + termsCount map[string]int + total int + missing int + sawValue bool +} + +func NewTermsFacetBuilder(field string, size int) *TermsFacetBuilder { + return &TermsFacetBuilder{ + size: size, + field: field, + termsCount: make(map[string]int), + } +} + +func (fb *TermsFacetBuilder) Size() int { + sizeInBytes := reflectStaticSizeTermsFacetBuilder + size.SizeOfPtr + + len(fb.field) + + for k := range fb.termsCount { + sizeInBytes += size.SizeOfString + len(k) + + size.SizeOfInt + } + + return sizeInBytes +} + +func (fb *TermsFacetBuilder) Field() string { + return fb.field +} + +func (fb *TermsFacetBuilder) UpdateVisitor(term []byte) { + fb.sawValue = true + fb.termsCount[string(term)] = fb.termsCount[string(term)] + 1 + fb.total++ +} + +func (fb *TermsFacetBuilder) StartDoc() { + fb.sawValue = false +} + +func (fb *TermsFacetBuilder) EndDoc() { + if !fb.sawValue { + fb.missing++ + } +} + +func (fb *TermsFacetBuilder) Result() *search.FacetResult { + rv := search.FacetResult{ + Field: fb.field, + Total: fb.total, + Missing: fb.missing, + } + + rv.Terms = &search.TermFacets{} + + for term, count := range fb.termsCount { + tf := &search.TermFacet{ + Term: term, + Count: count, + } + + rv.Terms.Add(tf) + } + + sort.Sort(rv.Terms) + + // we now have the list of the top N facets + trimTopN := fb.size + if trimTopN > rv.Terms.Len() { + trimTopN = rv.Terms.Len() + } + rv.Terms.TrimToTopN(trimTopN) + + notOther := 0 + for _, tf := range rv.Terms.Terms() { + notOther += tf.Count + } + rv.Other = fb.total - notOther + + return &rv +} diff --git a/search/facet/facet_builder_terms_test.go b/search/facet/facet_builder_terms_test.go new file mode 100644 index 0000000..c9f9d47 --- /dev/null +++ b/search/facet/facet_builder_terms_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package facet + +import ( + "os" + "regexp" + "testing" +) + +var terms []string + +func init() { + wsRegexp := regexp.MustCompile(`\W+`) + input, err := os.ReadFile("benchmark_data.txt") + if err != nil { + panic(err) + } + terms = wsRegexp.Split(string(input), -1) +} + +func BenchmarkTermsFacet10(b *testing.B) { + termsFacetN(b, 10) +} + +func BenchmarkTermsFacet100(b *testing.B) { + termsFacetN(b, 100) +} + +func BenchmarkTermsFacet1000(b *testing.B) { + termsFacetN(b, 1000) +} + +func BenchmarkTermsFacet10000(b *testing.B) { + termsFacetN(b, 10000) +} + +// func BenchmarkTermsFacet100000(b *testing.B) { +// termsFacetN(b, 100000) +// } + +func termsFacetN(b *testing.B, numTerms int) { + field := "test" + termsLen := len(terms) + tfb := NewTermsFacetBuilder(field, 3) + i := 0 + for len(tfb.termsCount) < numTerms && i <= termsLen { + j := i % termsLen + term := terms[j] + tfb.StartDoc() + tfb.UpdateVisitor([]byte(term)) + tfb.EndDoc() + i++ + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tfb.Result() + } +} diff --git a/search/facets_builder.go b/search/facets_builder.go new file mode 100644 index 0000000..4b1f2db --- /dev/null +++ b/search/facets_builder.go @@ -0,0 +1,411 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "reflect" + "sort" + + "github.com/blevesearch/bleve/v2/size" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeFacetsBuilder int +var reflectStaticSizeFacetResult int +var reflectStaticSizeTermFacet int +var reflectStaticSizeNumericRangeFacet int +var reflectStaticSizeDateRangeFacet int + +func init() { + var fb FacetsBuilder + reflectStaticSizeFacetsBuilder = int(reflect.TypeOf(fb).Size()) + var fr FacetResult + reflectStaticSizeFacetResult = int(reflect.TypeOf(fr).Size()) + var tf TermFacet + reflectStaticSizeTermFacet = int(reflect.TypeOf(tf).Size()) + var nrf NumericRangeFacet + reflectStaticSizeNumericRangeFacet = int(reflect.TypeOf(nrf).Size()) + var drf DateRangeFacet + reflectStaticSizeDateRangeFacet = int(reflect.TypeOf(drf).Size()) +} + +type FacetBuilder interface { + StartDoc() + UpdateVisitor(term []byte) + EndDoc() + + Result() *FacetResult + Field() string + + Size() int +} + +type FacetsBuilder struct { + indexReader index.IndexReader + facetNames []string + facets []FacetBuilder + facetsByField map[string][]FacetBuilder + fields []string +} + +func NewFacetsBuilder(indexReader index.IndexReader) *FacetsBuilder { + return &FacetsBuilder{ + indexReader: indexReader, + } +} + +func (fb *FacetsBuilder) Size() int { + sizeInBytes := reflectStaticSizeFacetsBuilder + size.SizeOfPtr + + for k, v := range fb.facets { + sizeInBytes += size.SizeOfString + v.Size() + len(fb.facetNames[k]) + } + + for _, entry := range fb.fields { + sizeInBytes += size.SizeOfString + len(entry) + } + + return sizeInBytes +} + +func (fb *FacetsBuilder) Add(name string, facetBuilder FacetBuilder) { + if fb.facetsByField == nil { + fb.facetsByField = map[string][]FacetBuilder{} + } + + fb.facetNames = append(fb.facetNames, name) + fb.facets = append(fb.facets, facetBuilder) + fb.facetsByField[facetBuilder.Field()] = append(fb.facetsByField[facetBuilder.Field()], facetBuilder) + fb.fields = append(fb.fields, facetBuilder.Field()) +} + +func (fb *FacetsBuilder) RequiredFields() []string { + return fb.fields +} + +func (fb *FacetsBuilder) StartDoc() { + for _, facetBuilder := range fb.facets { + facetBuilder.StartDoc() + } +} + +func (fb *FacetsBuilder) EndDoc() { + for _, facetBuilder := range fb.facets { + facetBuilder.EndDoc() + } +} + +func (fb *FacetsBuilder) UpdateVisitor(field string, term []byte) { + if facetBuilders, ok := fb.facetsByField[field]; ok { + for _, facetBuilder := range facetBuilders { + facetBuilder.UpdateVisitor(term) + } + } +} + +type TermFacet struct { + Term string `json:"term"` + Count int `json:"count"` +} + +type TermFacets struct { + termFacets []*TermFacet + termLookup map[string]*TermFacet +} + +func (tf *TermFacets) Terms() []*TermFacet { + if tf == nil { + return []*TermFacet{} + } + return tf.termFacets +} + +func (tf *TermFacets) TrimToTopN(n int) { + tf.termFacets = tf.termFacets[:n] +} + +func (tf *TermFacets) Add(termFacets ...*TermFacet) { + for _, termFacet := range termFacets { + if tf.termLookup == nil { + tf.termLookup = map[string]*TermFacet{} + } + + if term, ok := tf.termLookup[termFacet.Term]; ok { + term.Count += termFacet.Count + return + } + + // if we got here it wasn't already in the existing terms + tf.termFacets = append(tf.termFacets, termFacet) + tf.termLookup[termFacet.Term] = termFacet + } +} + +func (tf *TermFacets) Len() int { + // Handle case where *TermFacets is not fully initialized in index_impl.go.init() + if tf == nil { + return 0 + } + + return len(tf.termFacets) +} +func (tf *TermFacets) Swap(i, j int) { + tf.termFacets[i], tf.termFacets[j] = tf.termFacets[j], tf.termFacets[i] +} +func (tf *TermFacets) Less(i, j int) bool { + if tf.termFacets[i].Count == tf.termFacets[j].Count { + return tf.termFacets[i].Term < tf.termFacets[j].Term + } + return tf.termFacets[i].Count > tf.termFacets[j].Count +} + +// TermFacets used to be a type alias for []*TermFacet. +// To maintain backwards compatibility, we have to implement custom +// JSON marshalling. +func (tf *TermFacets) MarshalJSON() ([]byte, error) { + return util.MarshalJSON(tf.termFacets) +} + +func (tf *TermFacets) UnmarshalJSON(b []byte) error { + termFacets := []*TermFacet{} + err := util.UnmarshalJSON(b, &termFacets) + if err != nil { + return err + } + + for _, termFacet := range termFacets { + tf.Add(termFacet) + } + + return nil +} + +type NumericRangeFacet struct { + Name string `json:"name"` + Min *float64 `json:"min,omitempty"` + Max *float64 `json:"max,omitempty"` + Count int `json:"count"` +} + +func (nrf *NumericRangeFacet) Same(other *NumericRangeFacet) bool { + if nrf.Min == nil && other.Min != nil { + return false + } + if nrf.Min != nil && other.Min == nil { + return false + } + if nrf.Min != nil && other.Min != nil && *nrf.Min != *other.Min { + return false + } + if nrf.Max == nil && other.Max != nil { + return false + } + if nrf.Max != nil && other.Max == nil { + return false + } + if nrf.Max != nil && other.Max != nil && *nrf.Max != *other.Max { + return false + } + + return true +} + +type NumericRangeFacets []*NumericRangeFacet + +func (nrf NumericRangeFacets) Add(numericRangeFacet *NumericRangeFacet) NumericRangeFacets { + for _, existingNr := range nrf { + if numericRangeFacet.Same(existingNr) { + existingNr.Count += numericRangeFacet.Count + return nrf + } + } + // if we got here it wasn't already in the existing terms + nrf = append(nrf, numericRangeFacet) + return nrf +} + +func (nrf NumericRangeFacets) Len() int { return len(nrf) } +func (nrf NumericRangeFacets) Swap(i, j int) { nrf[i], nrf[j] = nrf[j], nrf[i] } +func (nrf NumericRangeFacets) Less(i, j int) bool { + if nrf[i].Count == nrf[j].Count { + return nrf[i].Name < nrf[j].Name + } + return nrf[i].Count > nrf[j].Count +} + +type DateRangeFacet struct { + Name string `json:"name"` + Start *string `json:"start,omitempty"` + End *string `json:"end,omitempty"` + Count int `json:"count"` +} + +func (drf *DateRangeFacet) Same(other *DateRangeFacet) bool { + if drf.Start == nil && other.Start != nil { + return false + } + if drf.Start != nil && other.Start == nil { + return false + } + if drf.Start != nil && other.Start != nil && *drf.Start != *other.Start { + return false + } + if drf.End == nil && other.End != nil { + return false + } + if drf.End != nil && other.End == nil { + return false + } + if drf.End != nil && other.End != nil && *drf.End != *other.End { + return false + } + + return true +} + +type DateRangeFacets []*DateRangeFacet + +func (drf DateRangeFacets) Add(dateRangeFacet *DateRangeFacet) DateRangeFacets { + for _, existingDr := range drf { + if dateRangeFacet.Same(existingDr) { + existingDr.Count += dateRangeFacet.Count + return drf + } + } + // if we got here it wasn't already in the existing terms + drf = append(drf, dateRangeFacet) + return drf +} + +func (drf DateRangeFacets) Len() int { return len(drf) } +func (drf DateRangeFacets) Swap(i, j int) { drf[i], drf[j] = drf[j], drf[i] } +func (drf DateRangeFacets) Less(i, j int) bool { + if drf[i].Count == drf[j].Count { + return drf[i].Name < drf[j].Name + } + return drf[i].Count > drf[j].Count +} + +type FacetResult struct { + Field string `json:"field"` + Total int `json:"total"` + Missing int `json:"missing"` + Other int `json:"other"` + Terms *TermFacets `json:"terms,omitempty"` + NumericRanges NumericRangeFacets `json:"numeric_ranges,omitempty"` + DateRanges DateRangeFacets `json:"date_ranges,omitempty"` +} + +func (fr *FacetResult) Size() int { + return reflectStaticSizeFacetResult + size.SizeOfPtr + + len(fr.Field) + + fr.Terms.Len()*(reflectStaticSizeTermFacet+size.SizeOfPtr) + + len(fr.NumericRanges)*(reflectStaticSizeNumericRangeFacet+size.SizeOfPtr) + + len(fr.DateRanges)*(reflectStaticSizeDateRangeFacet+size.SizeOfPtr) +} + +func (fr *FacetResult) Merge(other *FacetResult) { + fr.Total += other.Total + fr.Missing += other.Missing + fr.Other += other.Other + if other.Terms != nil { + if fr.Terms == nil { + fr.Terms = other.Terms + return + } + for _, term := range other.Terms.termFacets { + fr.Terms.Add(term) + } + } + if other.NumericRanges != nil { + if fr.NumericRanges == nil { + fr.NumericRanges = other.NumericRanges + return + } + for _, nr := range other.NumericRanges { + fr.NumericRanges = fr.NumericRanges.Add(nr) + } + } + if other.DateRanges != nil { + if fr.DateRanges == nil { + fr.DateRanges = other.DateRanges + return + } + for _, dr := range other.DateRanges { + fr.DateRanges = fr.DateRanges.Add(dr) + } + } +} + +func (fr *FacetResult) Fixup(size int) { + if fr.Terms != nil { + sort.Sort(fr.Terms) + if fr.Terms.Len() > size { + moveToOther := fr.Terms.termFacets[size:] + for _, mto := range moveToOther { + fr.Other += mto.Count + } + fr.Terms.termFacets = fr.Terms.termFacets[0:size] + } + } else if fr.NumericRanges != nil { + sort.Sort(fr.NumericRanges) + if len(fr.NumericRanges) > size { + moveToOther := fr.NumericRanges[size:] + for _, mto := range moveToOther { + fr.Other += mto.Count + } + fr.NumericRanges = fr.NumericRanges[0:size] + } + } else if fr.DateRanges != nil { + sort.Sort(fr.DateRanges) + if len(fr.DateRanges) > size { + moveToOther := fr.DateRanges[size:] + for _, mto := range moveToOther { + fr.Other += mto.Count + } + fr.DateRanges = fr.DateRanges[0:size] + } + } +} + +type FacetResults map[string]*FacetResult + +func (fr FacetResults) Merge(other FacetResults) { + for name, oFacetResult := range other { + facetResult, ok := fr[name] + if ok { + facetResult.Merge(oFacetResult) + } else { + fr[name] = oFacetResult + } + } +} + +func (fr FacetResults) Fixup(name string, size int) { + facetResult, ok := fr[name] + if ok { + facetResult.Fixup(size) + } +} + +func (fb *FacetsBuilder) Results() FacetResults { + fr := make(FacetResults) + for i, facetBuilder := range fb.facets { + facetResult := facetBuilder.Result() + fr[fb.facetNames[i]] = facetResult + } + return fr +} diff --git a/search/facets_builder_test.go b/search/facets_builder_test.go new file mode 100644 index 0000000..6815b1a --- /dev/null +++ b/search/facets_builder_test.go @@ -0,0 +1,424 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "fmt" + "reflect" + "testing" +) + +func TestTermFacetResultsMerge(t *testing.T) { + type testCase struct { + // Input + frs1 FacetResults // first facet results + frs2 FacetResults // second facet results (to be merged into first) + fixups map[string]int // {facetName:size} (to be applied after merge) + + // Expected output + expFrs FacetResults // facet results after merge and fixup + } + + tests := []*testCase{ + func() *testCase { + rv := &testCase{} + + rv.frs1 = FacetResults{ + "types": &FacetResult{ + Field: "type", + Total: 100, + Missing: 25, + Other: 25, + Terms: func() *TermFacets { + tfs := &TermFacets{} + tfs.Add( + &TermFacet{ + Term: "blog", + Count: 25, + }, + &TermFacet{ + Term: "comment", + Count: 24, + }, + &TermFacet{ + Term: "feedback", + Count: 1, + }, + ) + return tfs + }(), + }, + "categories": &FacetResult{ + Field: "category", + Total: 97, + Missing: 22, + Other: 15, + Terms: func() *TermFacets { + tfs := &TermFacets{} + tfs.Add( + &TermFacet{ + Term: "clothing", + Count: 35, + }, + &TermFacet{ + Term: "electronics", + Count: 25, + }, + ) + return tfs + }(), + }, + } + rv.frs2 = FacetResults{ + "types": &FacetResult{ + Field: "type", + Total: 100, + Missing: 25, + Other: 25, + Terms: func() *TermFacets { + tfs := &TermFacets{} + tfs.Add( + &TermFacet{ + Term: "blog", + Count: 25, + }, + &TermFacet{ + Term: "comment", + Count: 22, + }, + &TermFacet{ + Term: "flag", + Count: 3, + }, + ) + return tfs + }(), + }, + } + rv.fixups = map[string]int{ + "types": 3, // we want top 3 terms based on count + } + + rv.expFrs = FacetResults{ + "types": &FacetResult{ + Field: "type", + Total: 200, + Missing: 50, + Other: 51, + Terms: &TermFacets{ + termFacets: []*TermFacet{ + { + Term: "blog", + Count: 50, + }, + { + Term: "comment", + Count: 46, + }, + { + Term: "flag", + Count: 3, + }, + }, + }, + }, + "categories": rv.frs1["categories"], + } + + return rv + }(), + func() *testCase { + rv := &testCase{} + + rv.frs1 = FacetResults{ + "facetName": &FacetResult{ + Field: "docField", + Total: 0, + Missing: 0, + Other: 0, + Terms: nil, + }, + } + rv.frs2 = FacetResults{ + "facetName": &FacetResult{ + Field: "docField", + Total: 3, + Missing: 0, + Other: 0, + Terms: &TermFacets{ + termFacets: []*TermFacet{ + { + Term: "firstTerm", + Count: 1, + }, + { + Term: "secondTerm", + Count: 2, + }, + }, + }, + }, + } + rv.fixups = map[string]int{ + "facetName": 1, + } + + rv.expFrs = FacetResults{ + "facetName": &FacetResult{ + Field: "docField", + Total: 3, + Missing: 0, + Other: 1, + Terms: &TermFacets{ + termFacets: []*TermFacet{ + { + Term: "secondTerm", + Count: 2, + }, + }, + }, + }, + } + return rv + }(), + } + + for tcIdx, tc := range tests { + t.Run(fmt.Sprintf("T#%d", tcIdx), func(t *testing.T) { + tc.frs1.Merge(tc.frs2) + for facetName, size := range tc.fixups { + tc.frs1.Fixup(facetName, size) + } + + // clear termLookup, so we can compare the facet results + for _, fr := range tc.frs1 { + if fr.Terms != nil { + fr.Terms.termLookup = nil + } + } + + if !reflect.DeepEqual(tc.frs1, tc.expFrs) { + t.Errorf("expected %v, got %v", tc.expFrs, tc.frs1) + } + }) + } +} + +func TestNumericFacetResultsMerge(t *testing.T) { + + lowmed := 3.0 + medhi := 6.0 + hihigher := 9.0 + + // why second copy? the pointers may be different, but values the same + lowmed2 := 3.0 + medhi2 := 6.0 + hihigher2 := 9.0 + + fr1 := &FacetResult{ + Field: "rating", + Total: 100, + Missing: 25, + Other: 25, + NumericRanges: []*NumericRangeFacet{ + { + Name: "low", + Max: &lowmed, + Count: 25, + }, + { + Name: "med", + Count: 24, + Max: &lowmed, + Min: &medhi, + }, + { + Name: "hi", + Count: 1, + Min: &medhi, + Max: &hihigher, + }, + }, + } + frs1 := FacetResults{ + "ratings": fr1, + } + + fr2 := &FacetResult{ + Field: "rating", + Total: 100, + Missing: 25, + Other: 25, + NumericRanges: []*NumericRangeFacet{ + { + Name: "low", + Max: &lowmed2, + Count: 25, + }, + { + Name: "med", + Max: &lowmed2, + Min: &medhi2, + Count: 22, + }, + { + Name: "highest", + Min: &hihigher2, + Count: 3, + }, + }, + } + frs2 := FacetResults{ + "ratings": fr2, + } + + expectedFr := &FacetResult{ + Field: "rating", + Total: 200, + Missing: 50, + Other: 51, + NumericRanges: []*NumericRangeFacet{ + { + Name: "low", + Count: 50, + Max: &lowmed, + }, + { + Name: "med", + Max: &lowmed, + Min: &medhi, + Count: 46, + }, + { + Name: "highest", + Min: &hihigher, + Count: 3, + }, + }, + } + expectedFrs := FacetResults{ + "ratings": expectedFr, + } + + frs1.Merge(frs2) + frs1.Fixup("ratings", 3) + if !reflect.DeepEqual(frs1, expectedFrs) { + t.Errorf("expected %#v, got %#v", expectedFrs, frs1) + } +} + +func TestDateFacetResultsMerge(t *testing.T) { + + lowmed := "2010-01-01" + medhi := "2011-01-01" + hihigher := "2012-01-01" + + // why second copy? the pointer are to strings done by date time parsing + // inside the facet generation, so comparing pointers will not work + lowmed2 := "2010-01-01" + medhi2 := "2011-01-01" + hihigher2 := "2012-01-01" + + fr1 := &FacetResult{ + Field: "birthday", + Total: 100, + Missing: 25, + Other: 25, + DateRanges: []*DateRangeFacet{ + { + Name: "low", + End: &lowmed, + Count: 25, + }, + { + Name: "med", + Count: 24, + Start: &lowmed, + End: &medhi, + }, + { + Name: "hi", + Count: 1, + Start: &medhi, + End: &hihigher, + }, + }, + } + frs1 := FacetResults{ + "birthdays": fr1, + } + + fr2 := &FacetResult{ + Field: "birthday", + Total: 100, + Missing: 25, + Other: 25, + DateRanges: []*DateRangeFacet{ + { + Name: "low", + End: &lowmed2, + Count: 25, + }, + { + Name: "med", + Start: &lowmed2, + End: &medhi2, + Count: 22, + }, + { + Name: "highest", + Start: &hihigher2, + Count: 3, + }, + }, + } + frs2 := FacetResults{ + "birthdays": fr2, + } + + expectedFr := &FacetResult{ + Field: "birthday", + Total: 200, + Missing: 50, + Other: 51, + DateRanges: []*DateRangeFacet{ + { + Name: "low", + Count: 50, + End: &lowmed, + }, + { + Name: "med", + Start: &lowmed, + End: &medhi, + Count: 46, + }, + { + Name: "highest", + Start: &hihigher, + Count: 3, + }, + }, + } + expectedFrs := FacetResults{ + "birthdays": expectedFr, + } + + frs1.Merge(frs2) + frs1.Fixup("birthdays", 3) + if !reflect.DeepEqual(frs1, expectedFrs) { + t.Errorf("expected %#v, got %#v", expectedFrs, frs1) + } +} diff --git a/search/highlight/format/ansi/ansi.go b/search/highlight/format/ansi/ansi.go new file mode 100644 index 0000000..4a0e1eb --- /dev/null +++ b/search/highlight/format/ansi/ansi.go @@ -0,0 +1,112 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ansi + +import ( + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +const Name = "ansi" + +const DefaultAnsiHighlight = BgYellow + +type FragmentFormatter struct { + color string +} + +func NewFragmentFormatter(color string) *FragmentFormatter { + return &FragmentFormatter{ + color: color, + } +} + +func (a *FragmentFormatter) Format(f *highlight.Fragment, orderedTermLocations highlight.TermLocations) string { + rv := "" + curr := f.Start + for _, termLocation := range orderedTermLocations { + if termLocation == nil { + continue + } + // make sure the array positions match + if !termLocation.ArrayPositions.Equals(f.ArrayPositions) { + continue + } + if termLocation.Start < curr { + continue + } + if termLocation.End > f.End { + break + } + // add the stuff before this location + rv += string(f.Orig[curr:termLocation.Start]) + // add the color + rv += a.color + // add the term itself + rv += string(f.Orig[termLocation.Start:termLocation.End]) + // reset the color + rv += Reset + // update current + curr = termLocation.End + } + // add any remaining text after the last token + rv += string(f.Orig[curr:f.End]) + + return rv +} + +// ANSI color control escape sequences. +// Shamelessly copied from https://github.com/sqp/godock/blob/master/libs/log/colors.go +const ( + Reset = "\x1b[0m" + Bright = "\x1b[1m" + Dim = "\x1b[2m" + Underscore = "\x1b[4m" + Blink = "\x1b[5m" + Reverse = "\x1b[7m" + Hidden = "\x1b[8m" + FgBlack = "\x1b[30m" + FgRed = "\x1b[31m" + FgGreen = "\x1b[32m" + FgYellow = "\x1b[33m" + FgBlue = "\x1b[34m" + FgMagenta = "\x1b[35m" + FgCyan = "\x1b[36m" + FgWhite = "\x1b[37m" + BgBlack = "\x1b[40m" + BgRed = "\x1b[41m" + BgGreen = "\x1b[42m" + BgYellow = "\x1b[43m" + BgBlue = "\x1b[44m" + BgMagenta = "\x1b[45m" + BgCyan = "\x1b[46m" + BgWhite = "\x1b[47m" +) + +func Constructor(config map[string]interface{}, cache *registry.Cache) (highlight.FragmentFormatter, error) { + color := DefaultAnsiHighlight + colorVal, ok := config["color"].(string) + if ok { + color = colorVal + } + return NewFragmentFormatter(color), nil +} + +func init() { + err := registry.RegisterFragmentFormatter(Name, Constructor) + if err != nil { + panic(err) + } +} diff --git a/search/highlight/format/html/html.go b/search/highlight/format/html/html.go new file mode 100644 index 0000000..92b6f61 --- /dev/null +++ b/search/highlight/format/html/html.go @@ -0,0 +1,94 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package html + +import ( + "html" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +const Name = "html" + +const defaultHTMLHighlightBefore = "" +const defaultHTMLHighlightAfter = "" + +type FragmentFormatter struct { + before string + after string +} + +func NewFragmentFormatter(before, after string) *FragmentFormatter { + return &FragmentFormatter{ + before: before, + after: after, + } +} + +func (a *FragmentFormatter) Format(f *highlight.Fragment, orderedTermLocations highlight.TermLocations) string { + rv := "" + curr := f.Start + for _, termLocation := range orderedTermLocations { + if termLocation == nil { + continue + } + // make sure the array positions match + if !termLocation.ArrayPositions.Equals(f.ArrayPositions) { + continue + } + if termLocation.Start < curr { + continue + } + if termLocation.End > f.End { + break + } + // add the stuff before this location + rv += html.EscapeString(string(f.Orig[curr:termLocation.Start])) + // start the tag + rv += a.before + // add the term itself + rv += html.EscapeString(string(f.Orig[termLocation.Start:termLocation.End])) + // end the tag + rv += a.after + // update current + curr = termLocation.End + } + // add any remaining text after the last token + rv += html.EscapeString(string(f.Orig[curr:f.End])) + + return rv +} + +func Constructor(config map[string]interface{}, cache *registry.Cache) (highlight.FragmentFormatter, error) { + before := defaultHTMLHighlightBefore + beforeVal, ok := config["before"].(string) + if ok { + before = beforeVal + } + after := defaultHTMLHighlightAfter + afterVal, ok := config["after"].(string) + if ok { + after = afterVal + } + return NewFragmentFormatter(before, after), nil +} + +func init() { + err := registry.RegisterFragmentFormatter(Name, Constructor) + if err != nil { + panic(err) + } +} diff --git a/search/highlight/format/html/html_test.go b/search/highlight/format/html/html_test.go new file mode 100644 index 0000000..e746568 --- /dev/null +++ b/search/highlight/format/html/html_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package html + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +func TestHTMLFragmentFormatter(t *testing.T) { + tests := []struct { + fragment *highlight.Fragment + tlm search.TermLocationMap + output string + start string + end string + }{ + { + fragment: &highlight.Fragment{ + Orig: []byte("the quick brown fox"), + Start: 0, + End: 19, + }, + tlm: search.TermLocationMap{ + "quick": []*search.Location{ + { + Pos: 2, + Start: 4, + End: 9, + }, + }, + }, + output: "the quick brown fox", + start: "", + end: "", + }, + { + fragment: &highlight.Fragment{ + Orig: []byte("the quick brown fox"), + Start: 0, + End: 19, + }, + tlm: search.TermLocationMap{ + "quick": []*search.Location{ + { + Pos: 2, + Start: 4, + End: 9, + }, + }, + }, + output: "the quick brown fox", + start: "", + end: "", + }, + // test html escaping + { + fragment: &highlight.Fragment{ + Orig: []byte(" quick brown & fox"), + Start: 0, + End: 23, + }, + tlm: search.TermLocationMap{ + "quick": []*search.Location{ + { + Pos: 2, + Start: 6, + End: 11, + }, + }, + }, + output: "<the> quick brown & fox", + start: "", + end: "", + }, + // test html escaping inside search term + { + fragment: &highlight.Fragment{ + Orig: []byte(" qu&ick brown & fox"), + Start: 0, + End: 24, + }, + tlm: search.TermLocationMap{ + "qu&ick": []*search.Location{ + { + Pos: 2, + Start: 6, + End: 12, + }, + }, + }, + output: "<the> qu&ick brown & fox", + start: "", + end: "", + }, + } + + for _, test := range tests { + emHTMLFormatter := NewFragmentFormatter(test.start, test.end) + otl := highlight.OrderTermLocations(test.tlm) + result := emHTMLFormatter.Format(test.fragment, otl) + if result != test.output { + t.Errorf("expected `%s`, got `%s`", test.output, result) + } + } +} diff --git a/search/highlight/format/plain/plain.go b/search/highlight/format/plain/plain.go new file mode 100644 index 0000000..f65d1b7 --- /dev/null +++ b/search/highlight/format/plain/plain.go @@ -0,0 +1,92 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plain + +import ( + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +const Name = "plain" + +const defaultPlainHighlightBefore = "" +const defaultPlainHighlightAfter = "" + +type FragmentFormatter struct { + before string + after string +} + +func NewFragmentFormatter(before, after string) *FragmentFormatter { + return &FragmentFormatter{ + before: before, + after: after, + } +} + +func (a *FragmentFormatter) Format(f *highlight.Fragment, orderedTermLocations highlight.TermLocations) string { + rv := "" + curr := f.Start + for _, termLocation := range orderedTermLocations { + if termLocation == nil { + continue + } + // make sure the array positions match + if !termLocation.ArrayPositions.Equals(f.ArrayPositions) { + continue + } + if termLocation.Start < curr { + continue + } + if termLocation.End > f.End { + break + } + // add the stuff before this location + rv += string(f.Orig[curr:termLocation.Start]) + // start the highlight tag + rv += a.before + // add the term itself + rv += string(f.Orig[termLocation.Start:termLocation.End]) + // end the highlight tag + rv += a.after + // update current + curr = termLocation.End + } + // add any remaining text after the last token + rv += string(f.Orig[curr:f.End]) + + return rv +} + +func Constructor(config map[string]interface{}, cache *registry.Cache) (highlight.FragmentFormatter, error) { + before := defaultPlainHighlightBefore + beforeVal, ok := config["before"].(string) + if ok { + before = beforeVal + } + after := defaultPlainHighlightAfter + afterVal, ok := config["after"].(string) + if ok { + after = afterVal + } + return NewFragmentFormatter(before, after), nil +} + +func init() { + err := registry.RegisterFragmentFormatter(Name, Constructor) + if err != nil { + panic(err) + } +} diff --git a/search/highlight/format/plain/plain_test.go b/search/highlight/format/plain/plain_test.go new file mode 100644 index 0000000..7786f4e --- /dev/null +++ b/search/highlight/format/plain/plain_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plain + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +func TestPlainFragmentFormatter(t *testing.T) { + tests := []struct { + fragment *highlight.Fragment + tlm search.TermLocationMap + output string + start string + end string + }{ + { + fragment: &highlight.Fragment{ + Orig: []byte("the quick brown fox"), + Start: 0, + End: 19, + }, + tlm: search.TermLocationMap{ + "quick": []*search.Location{ + { + Pos: 2, + Start: 4, + End: 9, + }, + }, + }, + output: "the quick brown fox", + start: "", + end: "", + }, + { + fragment: &highlight.Fragment{ + Orig: []byte("the quick brown fox"), + Start: 0, + End: 19, + }, + tlm: search.TermLocationMap{ + "quick": []*search.Location{ + { + Pos: 2, + Start: 4, + End: 9, + }, + }, + }, + output: "the quick brown fox", + start: "", + end: "", + }, + } + + for _, test := range tests { + plainFormatter := NewFragmentFormatter(test.start, test.end) + otl := highlight.OrderTermLocations(test.tlm) + result := plainFormatter.Format(test.fragment, otl) + if result != test.output { + t.Errorf("expected `%s`, got `%s`", test.output, result) + } + } +} diff --git a/search/highlight/fragmenter/simple/simple.go b/search/highlight/fragmenter/simple/simple.go new file mode 100644 index 0000000..1c34b01 --- /dev/null +++ b/search/highlight/fragmenter/simple/simple.go @@ -0,0 +1,156 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package simple + +import ( + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +const Name = "simple" + +const defaultFragmentSize = 200 + +type Fragmenter struct { + fragmentSize int +} + +func NewFragmenter(fragmentSize int) *Fragmenter { + return &Fragmenter{ + fragmentSize: fragmentSize, + } +} + +func (s *Fragmenter) Fragment(orig []byte, ot highlight.TermLocations) []*highlight.Fragment { + var rv []*highlight.Fragment + maxbegin := 0 +OUTER: + for currTermIndex, termLocation := range ot { + // start with this + // it should be the highest scoring fragment with this term first + start := termLocation.Start + end := start + used := 0 + for end < len(orig) && used < s.fragmentSize { + r, size := utf8.DecodeRune(orig[end:]) + if r == utf8.RuneError { + continue OUTER // bail + } + end += size + used++ + } + + // if we still have more characters available to us + // push back towards beginning + // without cross maxbegin + for start > 0 && used < s.fragmentSize { + if start > len(orig) { + // bail if out of bounds, possibly due to token replacement + // e.g with a regexp replacement + continue OUTER + } + r, size := utf8.DecodeLastRune(orig[0:start]) + if r == utf8.RuneError { + continue OUTER // bail + } + if start-size >= maxbegin { + start -= size + used++ + } else { + break + } + } + + // however, we'd rather have the tokens centered more in the frag + // lets try to do that as best we can, without affecting the score + // find the end of the last term in this fragment + minend := end + for _, innerTermLocation := range ot[currTermIndex:] { + if innerTermLocation.End > end { + break + } + minend = innerTermLocation.End + } + + // find the smaller of the two rooms to move + roomToMove := utf8.RuneCount(orig[minend:end]) + roomToMoveStart := 0 + if start >= maxbegin { + roomToMoveStart = utf8.RuneCount(orig[maxbegin:start]) + } + if roomToMoveStart < roomToMove { + roomToMove = roomToMoveStart + } + + offset := roomToMove / 2 + + for offset > 0 { + r, size := utf8.DecodeLastRune(orig[0:start]) + if r == utf8.RuneError { + continue OUTER // bail + } + start -= size + + r, size = utf8.DecodeLastRune(orig[0:end]) + if r == utf8.RuneError { + continue OUTER // bail + } + end -= size + offset-- + } + + rv = append(rv, &highlight.Fragment{Orig: orig, Start: start - offset, End: end - offset}) + // set maxbegin to the end of the current term location + // so that next one won't back up to include it + maxbegin = termLocation.End + + } + if len(ot) == 0 { + // if there were no terms to highlight + // produce a single fragment from the beginning + start := 0 + end := start + used := 0 + for end < len(orig) && used < s.fragmentSize { + r, size := utf8.DecodeRune(orig[end:]) + if r == utf8.RuneError { + break + } + end += size + used++ + } + rv = append(rv, &highlight.Fragment{Orig: orig, Start: start, End: end}) + } + + return rv +} + +func Constructor(config map[string]interface{}, cache *registry.Cache) (highlight.Fragmenter, error) { + size := defaultFragmentSize + sizeVal, ok := config["size"].(float64) + if ok { + size = int(sizeVal) + } + return NewFragmenter(size), nil +} + +func init() { + err := registry.RegisterFragmenter(Name, Constructor) + if err != nil { + panic(err) + } +} diff --git a/search/highlight/fragmenter/simple/simple_test.go b/search/highlight/fragmenter/simple/simple_test.go new file mode 100644 index 0000000..5106b65 --- /dev/null +++ b/search/highlight/fragmenter/simple/simple_test.go @@ -0,0 +1,311 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package simple + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/search/highlight" +) + +func TestSimpleFragmenter(t *testing.T) { + + tests := []struct { + orig []byte + fragments []*highlight.Fragment + ot highlight.TermLocations + size int + }{ + { + orig: []byte("this is a test"), + fragments: []*highlight.Fragment{ + { + Orig: []byte("this is a test"), + Start: 0, + End: 14, + }, + }, + ot: highlight.TermLocations{ + &highlight.TermLocation{ + Term: "test", + Pos: 4, + Start: 10, + End: 14, + }, + }, + size: 100, + }, + { + orig: []byte("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"), + fragments: []*highlight.Fragment{ + { + Orig: []byte("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"), + Start: 0, + End: 100, + }, + }, + ot: highlight.TermLocations{ + &highlight.TermLocation{ + Term: "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + Pos: 1, + Start: 0, + End: 100, + }, + }, + size: 100, + }, + { + orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + fragments: []*highlight.Fragment{ + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 0, + End: 100, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 10, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 20, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 30, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 40, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 50, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 60, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 70, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 80, + End: 101, + }, + { + Orig: []byte("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"), + Start: 90, + End: 101, + }, + }, + ot: highlight.TermLocations{ + &highlight.TermLocation{ + Term: "0123456789", + Pos: 1, + Start: 0, + End: 10, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 2, + Start: 10, + End: 20, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 3, + Start: 20, + End: 30, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 4, + Start: 30, + End: 40, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 5, + Start: 40, + End: 50, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 6, + Start: 50, + End: 60, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 7, + Start: 60, + End: 70, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 8, + Start: 70, + End: 80, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 9, + Start: 80, + End: 90, + }, + &highlight.TermLocation{ + Term: "0123456789", + Pos: 10, + Start: 90, + End: 100, + }, + }, + size: 100, + }, + { + orig: []byte("[[पानी का स्वाद]] [[नीलेश रघुवंशी]] का कविता संग्रह हैं। इस कृति के लिए उन्हें २००४ में [[केदार सम्मान]] से सम्मानित किया गया है।{{केदार सम्मान से सम्मानित कृतियाँ}}"), + fragments: []*highlight.Fragment{ + { + Orig: []byte("[[पानी का स्वाद]] [[नीलेश रघुवंशी]] का कविता संग्रह हैं। इस कृति के लिए उन्हें २००४ में [[केदार सम्मान]] से सम्मानित किया गया है।{{केदार सम्मान से सम्मानित कृतियाँ}}"), + Start: 0, + End: 411, + }, + }, + ot: highlight.TermLocations{ + &highlight.TermLocation{ + Term: "पानी", + Pos: 1, + Start: 2, + End: 14, + }, + }, + size: 200, + }, + { + orig: []byte("交换机"), + fragments: []*highlight.Fragment{ + { + Orig: []byte("交换机"), + Start: 0, + End: 9, + }, + { + Orig: []byte("交换机"), + Start: 3, + End: 9, + }, + }, + ot: highlight.TermLocations{ + &highlight.TermLocation{ + Term: "交换", + Pos: 1, + Start: 0, + End: 6, + }, + &highlight.TermLocation{ + Term: "换机", + Pos: 2, + Start: 3, + End: 9, + }, + }, + size: 200, + }, + } + + for _, test := range tests { + fragmenter := NewFragmenter(test.size) + fragments := fragmenter.Fragment(test.orig, test.ot) + if !reflect.DeepEqual(fragments, test.fragments) { + t.Errorf("expected %#v, got %#v", test.fragments, fragments) + for _, fragment := range fragments { + t.Logf("frag: %s", fragment.Orig[fragment.Start:fragment.End]) + t.Logf("frag: %d - %d", fragment.Start, fragment.End) + } + } + } +} + +func TestSimpleFragmenterWithSize(t *testing.T) { + + tests := []struct { + orig []byte + fragments []*highlight.Fragment + ot highlight.TermLocations + }{ + { + orig: []byte("this is a test"), + fragments: []*highlight.Fragment{ + { + Orig: []byte("this is a test"), + Start: 0, + End: 5, + }, + { + Orig: []byte("this is a test"), + Start: 9, + End: 14, + }, + }, + ot: highlight.TermLocations{ + &highlight.TermLocation{ + Term: "this", + Pos: 1, + Start: 0, + End: 5, + }, + &highlight.TermLocation{ + Term: "test", + Pos: 4, + Start: 10, + End: 14, + }, + }, + }, + { + orig: []byte("避免出现 rune 越界问题"), + fragments: []*highlight.Fragment{ + { + Orig: []byte("避免出现 rune 越界问题"), + Start: 0, + End: 13, + }, + }, + ot: nil, + }, + } + + fragmenter := NewFragmenter(5) + for _, test := range tests { + fragments := fragmenter.Fragment(test.orig, test.ot) + if !reflect.DeepEqual(fragments, test.fragments) { + t.Errorf("expected %#v, got %#v", test.fragments, fragments) + for _, fragment := range fragments { + t.Logf("frag: %#v", fragment) + } + } + } +} diff --git a/search/highlight/highlighter.go b/search/highlight/highlighter.go new file mode 100644 index 0000000..3dd9ce0 --- /dev/null +++ b/search/highlight/highlighter.go @@ -0,0 +1,64 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package highlight + +import ( + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +type Fragment struct { + Orig []byte + ArrayPositions []uint64 + Start int + End int + Score float64 + Index int // used by heap +} + +func (f *Fragment) Overlaps(other *Fragment) bool { + if other.Start >= f.Start && other.Start < f.End { + return true + } else if f.Start >= other.Start && f.Start < other.End { + return true + } + return false +} + +type Fragmenter interface { + Fragment([]byte, TermLocations) []*Fragment +} + +type FragmentFormatter interface { + Format(f *Fragment, orderedTermLocations TermLocations) string +} + +type FragmentScorer interface { + Score(f *Fragment) float64 +} + +type Highlighter interface { + Fragmenter() Fragmenter + SetFragmenter(Fragmenter) + + FragmentFormatter() FragmentFormatter + SetFragmentFormatter(FragmentFormatter) + + Separator() string + SetSeparator(string) + + BestFragmentInField(*search.DocumentMatch, index.Document, string) string + BestFragmentsInField(*search.DocumentMatch, index.Document, string, int) []string +} diff --git a/search/highlight/highlighter/ansi/ansi.go b/search/highlight/highlighter/ansi/ansi.go new file mode 100644 index 0000000..99c1575 --- /dev/null +++ b/search/highlight/highlighter/ansi/ansi.go @@ -0,0 +1,53 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ansi + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search/highlight" + ansiFormatter "github.com/blevesearch/bleve/v2/search/highlight/format/ansi" + simpleFragmenter "github.com/blevesearch/bleve/v2/search/highlight/fragmenter/simple" + simpleHighlighter "github.com/blevesearch/bleve/v2/search/highlight/highlighter/simple" +) + +const Name = "ansi" + +func Constructor(config map[string]interface{}, cache *registry.Cache) (highlight.Highlighter, error) { + + fragmenter, err := cache.FragmenterNamed(simpleFragmenter.Name) + if err != nil { + return nil, fmt.Errorf("error building fragmenter: %v", err) + } + + formatter, err := cache.FragmentFormatterNamed(ansiFormatter.Name) + if err != nil { + return nil, fmt.Errorf("error building fragment formatter: %v", err) + } + + return simpleHighlighter.NewHighlighter( + fragmenter, + formatter, + simpleHighlighter.DefaultSeparator), + nil +} + +func init() { + err := registry.RegisterHighlighter(Name, Constructor) + if err != nil { + panic(err) + } +} diff --git a/search/highlight/highlighter/html/html.go b/search/highlight/highlighter/html/html.go new file mode 100644 index 0000000..02eca0a --- /dev/null +++ b/search/highlight/highlighter/html/html.go @@ -0,0 +1,53 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package html + +import ( + "fmt" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search/highlight" + htmlFormatter "github.com/blevesearch/bleve/v2/search/highlight/format/html" + simpleFragmenter "github.com/blevesearch/bleve/v2/search/highlight/fragmenter/simple" + simpleHighlighter "github.com/blevesearch/bleve/v2/search/highlight/highlighter/simple" +) + +const Name = "html" + +func Constructor(config map[string]interface{}, cache *registry.Cache) (highlight.Highlighter, error) { + + fragmenter, err := cache.FragmenterNamed(simpleFragmenter.Name) + if err != nil { + return nil, fmt.Errorf("error building fragmenter: %v", err) + } + + formatter, err := cache.FragmentFormatterNamed(htmlFormatter.Name) + if err != nil { + return nil, fmt.Errorf("error building fragment formatter: %v", err) + } + + return simpleHighlighter.NewHighlighter( + fragmenter, + formatter, + simpleHighlighter.DefaultSeparator), + nil +} + +func init() { + err := registry.RegisterHighlighter(Name, Constructor) + if err != nil { + panic(err) + } +} diff --git a/search/highlight/highlighter/simple/fragment_scorer_simple.go b/search/highlight/highlighter/simple/fragment_scorer_simple.go new file mode 100644 index 0000000..786e33c --- /dev/null +++ b/search/highlight/highlighter/simple/fragment_scorer_simple.go @@ -0,0 +1,49 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package simple + +import ( + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +// FragmentScorer will score fragments by how many +// unique terms occur in the fragment with no regard for +// any boost values used in the original query +type FragmentScorer struct { + tlm search.TermLocationMap +} + +func NewFragmentScorer(tlm search.TermLocationMap) *FragmentScorer { + return &FragmentScorer{ + tlm: tlm, + } +} + +func (s *FragmentScorer) Score(f *highlight.Fragment) { + score := 0.0 +OUTER: + for _, locations := range s.tlm { + for _, location := range locations { + if location.ArrayPositions.Equals(f.ArrayPositions) && int(location.Start) >= f.Start && int(location.End) <= f.End { + score += 1.0 + // once we find a term in the fragment + // don't care about additional matches + continue OUTER + } + } + } + f.Score = score +} diff --git a/search/highlight/highlighter/simple/fragment_scorer_simple_test.go b/search/highlight/highlighter/simple/fragment_scorer_simple_test.go new file mode 100644 index 0000000..116c7bd --- /dev/null +++ b/search/highlight/highlighter/simple/fragment_scorer_simple_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package simple + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +func TestSimpleFragmentScorer(t *testing.T) { + + tests := []struct { + fragment *highlight.Fragment + tlm search.TermLocationMap + score float64 + }{ + { + fragment: &highlight.Fragment{ + Orig: []byte("cat in the hat"), + Start: 0, + End: 14, + }, + tlm: search.TermLocationMap{ + "cat": []*search.Location{ + { + Pos: 0, + Start: 0, + End: 3, + }, + }, + }, + score: 1, + }, + { + fragment: &highlight.Fragment{ + Orig: []byte("cat in the hat"), + Start: 0, + End: 14, + }, + tlm: search.TermLocationMap{ + "cat": []*search.Location{ + { + Pos: 1, + Start: 0, + End: 3, + }, + }, + "hat": []*search.Location{ + { + Pos: 4, + Start: 11, + End: 14, + }, + }, + }, + score: 2, + }, + } + + for _, test := range tests { + scorer := NewFragmentScorer(test.tlm) + scorer.Score(test.fragment) + if test.fragment.Score != test.score { + t.Errorf("expected score %f, got %f", test.score, test.fragment.Score) + } + } + +} diff --git a/search/highlight/highlighter/simple/highlighter_simple.go b/search/highlight/highlighter/simple/highlighter_simple.go new file mode 100644 index 0000000..e898a1e --- /dev/null +++ b/search/highlight/highlighter/simple/highlighter_simple.go @@ -0,0 +1,225 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package simple + +import ( + "container/heap" + "fmt" + + index "github.com/blevesearch/bleve_index_api" + + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight" +) + +const Name = "simple" +const DefaultSeparator = "…" + +type Highlighter struct { + fragmenter highlight.Fragmenter + formatter highlight.FragmentFormatter + sep string +} + +func NewHighlighter(fragmenter highlight.Fragmenter, formatter highlight.FragmentFormatter, separator string) *Highlighter { + return &Highlighter{ + fragmenter: fragmenter, + formatter: formatter, + sep: separator, + } +} + +func (s *Highlighter) Fragmenter() highlight.Fragmenter { + return s.fragmenter +} + +func (s *Highlighter) SetFragmenter(f highlight.Fragmenter) { + s.fragmenter = f +} + +func (s *Highlighter) FragmentFormatter() highlight.FragmentFormatter { + return s.formatter +} + +func (s *Highlighter) SetFragmentFormatter(f highlight.FragmentFormatter) { + s.formatter = f +} + +func (s *Highlighter) Separator() string { + return s.sep +} + +func (s *Highlighter) SetSeparator(sep string) { + s.sep = sep +} + +func (s *Highlighter) BestFragmentInField(dm *search.DocumentMatch, doc index.Document, field string) string { + fragments := s.BestFragmentsInField(dm, doc, field, 1) + if len(fragments) > 0 { + return fragments[0] + } + return "" +} + +func (s *Highlighter) BestFragmentsInField(dm *search.DocumentMatch, doc index.Document, field string, num int) []string { + tlm := dm.Locations[field] + orderedTermLocations := highlight.OrderTermLocations(tlm) + scorer := NewFragmentScorer(tlm) + + // score the fragments and put them into a priority queue ordered by score + fq := make(FragmentQueue, 0) + heap.Init(&fq) + doc.VisitFields(func(f index.Field) { + if f.Name() == field { + _, ok := f.(index.TextField) + if ok { + termLocationsSameArrayPosition := make(highlight.TermLocations, 0) + for _, otl := range orderedTermLocations { + if otl.ArrayPositions.Equals(f.ArrayPositions()) { + termLocationsSameArrayPosition = append(termLocationsSameArrayPosition, otl) + } + } + + fieldData := f.Value() + fragments := s.fragmenter.Fragment(fieldData, termLocationsSameArrayPosition) + for _, fragment := range fragments { + fragment.ArrayPositions = f.ArrayPositions() + scorer.Score(fragment) + heap.Push(&fq, fragment) + } + } + } + }) + + // now find the N best non-overlapping fragments + var bestFragments []*highlight.Fragment + if len(fq) > 0 { + candidate := heap.Pop(&fq) + OUTER: + for candidate != nil && len(bestFragments) < num { + // see if this overlaps with any of the best already identified + if len(bestFragments) > 0 { + for _, frag := range bestFragments { + if candidate.(*highlight.Fragment).Overlaps(frag) { + if len(fq) < 1 { + break OUTER + } + candidate = heap.Pop(&fq) + continue OUTER + } + } + bestFragments = append(bestFragments, candidate.(*highlight.Fragment)) + } else { + bestFragments = append(bestFragments, candidate.(*highlight.Fragment)) + } + + if len(fq) < 1 { + break + } + candidate = heap.Pop(&fq) + } + } + + // now that we have the best fragments, we can format them + orderedTermLocations.MergeOverlapping() + formattedFragments := make([]string, len(bestFragments)) + for i, fragment := range bestFragments { + formattedFragments[i] = "" + if fragment.Start != 0 { + formattedFragments[i] += s.sep + } + formattedFragments[i] += s.formatter.Format(fragment, orderedTermLocations) + if fragment.End != len(fragment.Orig) { + formattedFragments[i] += s.sep + } + } + + if dm.Fragments == nil { + dm.Fragments = make(search.FieldFragmentMap, 0) + } + if len(formattedFragments) > 0 { + dm.Fragments[field] = formattedFragments + } + + return formattedFragments +} + +// FragmentQueue implements heap.Interface and holds Items. +type FragmentQueue []*highlight.Fragment + +func (fq FragmentQueue) Len() int { return len(fq) } + +func (fq FragmentQueue) Less(i, j int) bool { + // We want Pop to give us the highest, not lowest, priority so we use greater-than here. + return fq[i].Score > fq[j].Score +} + +func (fq FragmentQueue) Swap(i, j int) { + fq[i], fq[j] = fq[j], fq[i] + fq[i].Index = i + fq[j].Index = j +} + +func (fq *FragmentQueue) Push(x interface{}) { + n := len(*fq) + item := x.(*highlight.Fragment) + item.Index = n + *fq = append(*fq, item) +} + +func (fq *FragmentQueue) Pop() interface{} { + old := *fq + n := len(old) + item := old[n-1] + item.Index = -1 // for safety + *fq = old[0 : n-1] + return item +} + +func Constructor(config map[string]interface{}, cache *registry.Cache) (highlight.Highlighter, error) { + separator := DefaultSeparator + separatorVal, ok := config["separator"].(string) + if ok { + separator = separatorVal + } + + fragmenterName, ok := config["fragmenter"].(string) + if !ok { + return nil, fmt.Errorf("must specify fragmenter") + } + fragmenter, err := cache.FragmenterNamed(fragmenterName) + if err != nil { + return nil, fmt.Errorf("error building fragmenter: %v", err) + } + + formatterName, ok := config["formatter"].(string) + if !ok { + return nil, fmt.Errorf("must specify formatter") + } + formatter, err := cache.FragmentFormatterNamed(formatterName) + if err != nil { + return nil, fmt.Errorf("error building fragment formatter: %v", err) + } + + return NewHighlighter(fragmenter, formatter, separator), nil +} + +func init() { + err := registry.RegisterHighlighter(Name, Constructor) + if err != nil { + panic(err) + } +} diff --git a/search/highlight/highlighter/simple/highlighter_simple_test.go b/search/highlight/highlighter/simple/highlighter_simple_test.go new file mode 100644 index 0000000..e93166a --- /dev/null +++ b/search/highlight/highlighter/simple/highlighter_simple_test.go @@ -0,0 +1,169 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package simple + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight/format/ansi" + sfrag "github.com/blevesearch/bleve/v2/search/highlight/fragmenter/simple" +) + +const ( + reset = "\x1b[0m" + DefaultAnsiHighlight = "\x1b[43m" +) + +func TestSimpleHighlighter(t *testing.T) { + fragmenter := sfrag.NewFragmenter(100) + formatter := ansi.NewFragmentFormatter(ansi.DefaultAnsiHighlight) + highlighter := NewHighlighter(fragmenter, formatter, DefaultSeparator) + + docMatch := search.DocumentMatch{ + ID: "a", + Score: 1.0, + Locations: search.FieldTermLocationMap{ + "desc": search.TermLocationMap{ + "quick": []*search.Location{ + { + Pos: 2, + Start: 4, + End: 9, + }, + }, + "fox": []*search.Location{ + { + Pos: 4, + Start: 16, + End: 19, + }, + }, + }, + }, + } + + expectedFragment := "the " + DefaultAnsiHighlight + "quick" + reset + " brown " + DefaultAnsiHighlight + "fox" + reset + " jumps over the lazy dog" + doc := document.NewDocument("a").AddField(document.NewTextField("desc", []uint64{}, []byte("the quick brown fox jumps over the lazy dog"))) + + fragment := highlighter.BestFragmentInField(&docMatch, doc, "desc") + if fragment != expectedFragment { + t.Errorf("expected `%s`, got `%s`", expectedFragment, fragment) + } +} + +func TestSimpleHighlighterLonger(t *testing.T) { + + fieldBytes := []byte(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed semper nulla, sed pellentesque urna. Suspendisse potenti. Aliquam dignissim pulvinar erat vel ullamcorper. Nullam sed diam at dolor dapibus varius. Vestibulum at semper nunc. Integer ullamcorper enim ut nisi condimentum lacinia. Nulla ipsum ipsum, dictum in dapibus non, bibendum eget neque. Vestibulum malesuada erat quis malesuada dictum. Mauris luctus viverra lorem, nec hendrerit lacus lacinia ut. Donec suscipit sit amet nisi et dictum. Maecenas ultrices mollis diam, vel commodo libero lobortis nec. Nunc non dignissim dolor. Nulla non tempus risus, eget porttitor lectus. Suspendisse vitae gravida magna, a sagittis urna. Curabitur nec dui volutpat, hendrerit nisi non, adipiscing erat. Maecenas aliquet sem sit amet nibh ultrices accumsan. + +Mauris lobortis sem sed blandit bibendum. In scelerisque eros sed metus aliquet convallis ac eget metus. Donec eget feugiat sem. Quisque venenatis, augue et blandit vulputate, velit odio viverra dolor, eu iaculis eros urna ut nunc. Duis faucibus mattis enim ut ultricies. Donec scelerisque volutpat elit, vel varius ante porttitor vel. Duis neque nulla, ultrices vel est id, molestie semper odio. Maecenas condimentum felis vitae nibh venenatis, ut feugiat risus vehicula. Suspendisse non sapien neque. Etiam et lorem consequat lorem aliquam ullamcorper. Pellentesque id vestibulum neque, at aliquam turpis. Aenean ultrices nec erat sit amet aliquam. Morbi eu sem in augue cursus ullamcorper a sed dolor. Integer et lobortis nulla, sit amet laoreet elit. In elementum, nibh nec volutpat pretium, lectus est pulvinar arcu, vehicula lobortis tellus sem id mauris. Maecenas ac blandit purus, sit amet scelerisque magna. + +In hac habitasse platea dictumst. In lacinia elit non risus venenatis viverra. Nulla vestibulum laoreet turpis ac accumsan. Vivamus eros felis, rhoncus vel interdum bibendum, imperdiet nec diam. Etiam sed eros sed orci pellentesque sagittis. Praesent a fermentum leo. Vivamus ipsum risus, faucibus a dignissim ut, ullamcorper nec risus. Etiam quis adipiscing velit. Nam ac cursus arcu. Sed bibendum lectus quis massa dapibus dapibus. Vestibulum fermentum eros vitae hendrerit condimentum. + +Fusce viverra eleifend iaculis. Maecenas tempor dictum cursus. Mauris faucibus, tortor in bibendum ornare, nibh lorem sollicitudin est, sed consectetur nulla dui imperdiet urna. Fusce aliquet odio fermentum massa mollis, id feugiat lacus egestas. Integer et eleifend metus. Duis neque tellus, vulputate nec dui eu, euismod sodales orci. Vivamus turpis erat, consectetur et pulvinar nec, ornare a quam. Maecenas fermentum, ligula vitae consectetur lobortis, mi lacus fermentum ante, ut semper lacus lectus porta orci. Nulla vehicula sodales eros, in iaculis ante laoreet at. Sed venenatis interdum metus, egestas scelerisque orci laoreet ut. Donec fermentum enim eget nibh blandit laoreet. Proin lacinia adipiscing lorem vel ornare. Donec ullamcorper massa elementum urna varius viverra. Proin pharetra, erat at feugiat rhoncus, velit eros condimentum mi, ac mattis sapien dolor non elit. Aenean viverra purus id tincidunt vulputate. + +Etiam vel augue vel nisl commodo suscipit et ac nisl. Quisque eros diam, porttitor et aliquet sed, vulputate in odio. Aenean feugiat est quis neque vehicula, eget vulputate nunc tempor. Donec quis nulla ut quam feugiat consectetur ut et justo. Nulla congue, metus auctor facilisis scelerisque, nunc risus vulputate urna, in blandit urna nibh et neque. Etiam quis tortor ut nulla dignissim dictum non sed ligula. Vivamus accumsan ligula eget ipsum ultrices, a tincidunt urna blandit. In hac habitasse platea dictumst.`) + + doc := document.NewDocument("a").AddField(document.NewTextField("full", []uint64{}, fieldBytes)) + docMatch := search.DocumentMatch{ + ID: "a", + Score: 1.0, + Locations: search.FieldTermLocationMap{ + "full": search.TermLocationMap{ + "metus": []*search.Location{ + { + Pos: 0, + Start: 883, + End: 888, + }, + { + Pos: 0, + Start: 915, + End: 920, + }, + { + Pos: 0, + Start: 2492, + End: 2497, + }, + { + Pos: 0, + Start: 2822, + End: 2827, + }, + { + Pos: 0, + Start: 3417, + End: 3422, + }, + }, + "interdum": []*search.Location{ + { + Pos: 0, + Start: 1891, + End: 1899, + }, + { + Pos: 0, + Start: 2813, + End: 2821, + }, + }, + "venenatis": []*search.Location{ + { + Pos: 0, + Start: 954, + End: 963, + }, + { + Pos: 0, + Start: 1252, + End: 1261, + }, + { + Pos: 0, + Start: 1795, + End: 1804, + }, + { + Pos: 0, + Start: 2803, + End: 2812, + }, + }, + }, + }, + } + + expectedFragments := []string{ + "…eros, in iaculis ante laoreet at. Sed " + DefaultAnsiHighlight + "venenatis" + reset + " " + DefaultAnsiHighlight + "interdum" + reset + " " + DefaultAnsiHighlight + "metus" + reset + ", egestas scelerisque orci laoreet ut.…", + "… eros sed " + DefaultAnsiHighlight + "metus" + reset + " aliquet convallis ac eget " + DefaultAnsiHighlight + "metus" + reset + ". Donec eget feugiat sem. Quisque " + DefaultAnsiHighlight + "venenatis" + reset + ", augue et…", + "… odio. Maecenas condimentum felis vitae nibh " + DefaultAnsiHighlight + "venenatis" + reset + ", ut feugiat risus vehicula. Suspendisse non s…", + "… id feugiat lacus egestas. Integer et eleifend " + DefaultAnsiHighlight + "metus" + reset + ". Duis neque tellus, vulputate nec dui eu, euism…", + "… accumsan. Vivamus eros felis, rhoncus vel " + DefaultAnsiHighlight + "interdum" + reset + " bibendum, imperdiet nec diam. Etiam sed eros sed…", + } + + fragmenter := sfrag.NewFragmenter(100) + formatter := ansi.NewFragmentFormatter(ansi.DefaultAnsiHighlight) + highlighter := NewHighlighter(fragmenter, formatter, DefaultSeparator) + fragments := highlighter.BestFragmentsInField(&docMatch, doc, "full", 5) + + if !reflect.DeepEqual(fragments, expectedFragments) { + t.Errorf("expected %#v, got %#v", expectedFragments, fragments) + } + +} diff --git a/search/highlight/term_locations.go b/search/highlight/term_locations.go new file mode 100644 index 0000000..6bf385c --- /dev/null +++ b/search/highlight/term_locations.go @@ -0,0 +1,105 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package highlight + +import ( + "reflect" + "sort" + + "github.com/blevesearch/bleve/v2/search" +) + +type TermLocation struct { + Term string + ArrayPositions search.ArrayPositions + Pos int + Start int + End int +} + +func (tl *TermLocation) Overlaps(other *TermLocation) bool { + if reflect.DeepEqual(tl.ArrayPositions, other.ArrayPositions) { + if other.Start >= tl.Start && other.Start < tl.End { + return true + } else if tl.Start >= other.Start && tl.Start < other.End { + return true + } + } + return false +} + +type TermLocations []*TermLocation + +func (t TermLocations) Len() int { return len(t) } +func (t TermLocations) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t TermLocations) Less(i, j int) bool { + + shortestArrayPositions := len(t[i].ArrayPositions) + if len(t[j].ArrayPositions) < shortestArrayPositions { + shortestArrayPositions = len(t[j].ArrayPositions) + } + + // compare all the common array positions + for api := 0; api < shortestArrayPositions; api++ { + if t[i].ArrayPositions[api] < t[j].ArrayPositions[api] { + return true + } + if t[i].ArrayPositions[api] > t[j].ArrayPositions[api] { + return false + } + } + // all the common array positions are the same + if len(t[i].ArrayPositions) < len(t[j].ArrayPositions) { + return true // j array positions, longer so greater + } else if len(t[i].ArrayPositions) > len(t[j].ArrayPositions) { + return false // j array positions, shorter so less + } + + // array positions the same, compare starts + return t[i].Start < t[j].Start +} + +func (t TermLocations) MergeOverlapping() { + var lastTl *TermLocation + for i, tl := range t { + if lastTl == nil && tl != nil { + lastTl = tl + } else if lastTl != nil && tl != nil { + if lastTl.Overlaps(tl) { + // ok merge this with previous + lastTl.End = tl.End + t[i] = nil + } + } + } +} + +func OrderTermLocations(tlm search.TermLocationMap) TermLocations { + rv := make(TermLocations, 0) + for term, locations := range tlm { + for _, location := range locations { + tl := TermLocation{ + Term: term, + ArrayPositions: location.ArrayPositions, + Pos: int(location.Pos), + Start: int(location.Start), + End: int(location.End), + } + rv = append(rv, &tl) + } + } + sort.Sort(rv) + return rv +} diff --git a/search/highlight/term_locations_test.go b/search/highlight/term_locations_test.go new file mode 100644 index 0000000..6daa5cc --- /dev/null +++ b/search/highlight/term_locations_test.go @@ -0,0 +1,512 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package highlight + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/search" +) + +func TestTermLocationOverlaps(t *testing.T) { + + tests := []struct { + left *TermLocation + right *TermLocation + expected bool + }{ + { + left: &TermLocation{ + Start: 0, + End: 5, + }, + right: &TermLocation{ + Start: 3, + End: 7, + }, + expected: true, + }, + { + left: &TermLocation{ + Start: 0, + End: 5, + }, + right: &TermLocation{ + Start: 5, + End: 7, + }, + expected: false, + }, + { + left: &TermLocation{ + Start: 0, + End: 5, + }, + right: &TermLocation{ + Start: 7, + End: 11, + }, + expected: false, + }, + // with array positions + { + left: &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + right: &TermLocation{ + ArrayPositions: search.ArrayPositions{1}, + Start: 7, + End: 11, + }, + expected: false, + }, + { + left: &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + right: &TermLocation{ + ArrayPositions: search.ArrayPositions{1}, + Start: 3, + End: 11, + }, + expected: false, + }, + { + left: &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + right: &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 3, + End: 11, + }, + expected: true, + }, + { + left: &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + right: &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 7, + End: 11, + }, + expected: false, + }, + } + + for _, test := range tests { + actual := test.left.Overlaps(test.right) + if actual != test.expected { + t.Errorf("expected %t got %t for %#v", test.expected, actual, test) + } + } +} + +func TestTermLocationsMergeOverlapping(t *testing.T) { + + tests := []struct { + input TermLocations + output TermLocations + }{ + { + input: TermLocations{}, + output: TermLocations{}, + }, + { + input: TermLocations{ + &TermLocation{ + Start: 0, + End: 5, + }, + &TermLocation{ + Start: 7, + End: 11, + }, + }, + output: TermLocations{ + &TermLocation{ + Start: 0, + End: 5, + }, + &TermLocation{ + Start: 7, + End: 11, + }, + }, + }, + { + input: TermLocations{ + &TermLocation{ + Start: 0, + End: 5, + }, + &TermLocation{ + Start: 4, + End: 11, + }, + }, + output: TermLocations{ + &TermLocation{ + Start: 0, + End: 11, + }, + nil, + }, + }, + { + input: TermLocations{ + &TermLocation{ + Start: 0, + End: 5, + }, + &TermLocation{ + Start: 4, + End: 11, + }, + &TermLocation{ + Start: 9, + End: 13, + }, + }, + output: TermLocations{ + &TermLocation{ + Start: 0, + End: 13, + }, + nil, + nil, + }, + }, + { + input: TermLocations{ + &TermLocation{ + Start: 0, + End: 5, + }, + &TermLocation{ + Start: 4, + End: 11, + }, + &TermLocation{ + Start: 9, + End: 13, + }, + &TermLocation{ + Start: 15, + End: 21, + }, + }, + output: TermLocations{ + &TermLocation{ + Start: 0, + End: 13, + }, + nil, + nil, + &TermLocation{ + Start: 15, + End: 21, + }, + }, + }, + // with array positions + { + input: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{1}, + Start: 7, + End: 11, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{1}, + Start: 7, + End: 11, + }, + }, + }, + { + input: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 7, + End: 11, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 7, + End: 11, + }, + }, + }, + { + input: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 3, + End: 11, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 11, + }, + nil, + }, + }, + { + input: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{1}, + Start: 3, + End: 11, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + End: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{1}, + Start: 3, + End: 11, + }, + }, + }, + } + + for _, test := range tests { + test.input.MergeOverlapping() + if !reflect.DeepEqual(test.input, test.output) { + t.Errorf("expected: %#v got %#v", test.output, test.input) + } + } +} + +func TestTermLocationsOrder(t *testing.T) { + + tests := []struct { + input search.TermLocationMap + output TermLocations + }{ + { + input: search.TermLocationMap{}, + output: TermLocations{}, + }, + { + input: search.TermLocationMap{ + "term": []*search.Location{ + { + Start: 0, + }, + { + Start: 5, + }, + }, + }, + output: TermLocations{ + &TermLocation{ + Term: "term", + Start: 0, + }, + &TermLocation{ + Term: "term", + Start: 5, + }, + }, + }, + { + input: search.TermLocationMap{ + "term": []*search.Location{ + { + Start: 5, + }, + { + Start: 0, + }, + }, + }, + output: TermLocations{ + &TermLocation{ + Term: "term", + Start: 0, + }, + &TermLocation{ + Term: "term", + Start: 5, + }, + }, + }, + // with array positions + { + input: search.TermLocationMap{ + "term": []*search.Location{ + { + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + }, + { + ArrayPositions: search.ArrayPositions{0}, + Start: 5, + }, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Term: "term", + Start: 0, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Term: "term", + Start: 5, + }, + }, + }, + { + input: search.TermLocationMap{ + "term": []*search.Location{ + { + ArrayPositions: search.ArrayPositions{0}, + Start: 5, + }, + { + ArrayPositions: search.ArrayPositions{0}, + Start: 0, + }, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Term: "term", + Start: 0, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Term: "term", + Start: 5, + }, + }, + }, + { + input: search.TermLocationMap{ + "term": []*search.Location{ + { + ArrayPositions: search.ArrayPositions{0}, + Start: 5, + }, + { + ArrayPositions: search.ArrayPositions{1}, + Start: 0, + }, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Term: "term", + Start: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{1}, + Term: "term", + Start: 0, + }, + }, + }, + { + input: search.TermLocationMap{ + "term": []*search.Location{ + { + ArrayPositions: search.ArrayPositions{0}, + Start: 5, + }, + { + ArrayPositions: search.ArrayPositions{0, 1}, + Start: 0, + }, + }, + }, + output: TermLocations{ + &TermLocation{ + ArrayPositions: search.ArrayPositions{0}, + Term: "term", + Start: 5, + }, + &TermLocation{ + ArrayPositions: search.ArrayPositions{0, 1}, + Term: "term", + Start: 0, + }, + }, + }, + } + + for _, test := range tests { + actual := OrderTermLocations(test.input) + if !reflect.DeepEqual(actual, test.output) { + t.Errorf("expected: %#v got %#v", test.output, actual) + } + } +} diff --git a/search/levenshtein.go b/search/levenshtein.go new file mode 100644 index 0000000..dadab25 --- /dev/null +++ b/search/levenshtein.go @@ -0,0 +1,118 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "math" +) + +func LevenshteinDistance(a, b string) int { + la := len(a) + lb := len(b) + d := make([]int, la+1) + var lastdiag, olddiag, temp int + + for i := 1; i <= la; i++ { + d[i] = i + } + for i := 1; i <= lb; i++ { + d[0] = i + lastdiag = i - 1 + for j := 1; j <= la; j++ { + olddiag = d[j] + min := d[j] + 1 + if (d[j-1] + 1) < min { + min = d[j-1] + 1 + } + if a[j-1] == b[i-1] { + temp = 0 + } else { + temp = 1 + } + if (lastdiag + temp) < min { + min = lastdiag + temp + } + d[j] = min + lastdiag = olddiag + } + } + return d[la] +} + +// LevenshteinDistanceMax same as LevenshteinDistance but +// attempts to bail early once we know the distance +// will be greater than max +// in which case the first return val will be the max +// and the second will be true, indicating max was exceeded +func LevenshteinDistanceMax(a, b string, max int) (int, bool) { + v, wasMax, _ := LevenshteinDistanceMaxReuseSlice(a, b, max, nil) + return v, wasMax +} + +func LevenshteinDistanceMaxReuseSlice(a, b string, max int, d []int) (int, bool, []int) { + la := len(a) + lb := len(b) + + ld := int(math.Abs(float64(la - lb))) + if ld > max { + return max, true, d + } else if la == 0 || lb == 0 { + // if one string of the two strings is empty, then ld is + // the length of the other string and as such is <= max + return ld, false, d + } + + if cap(d) < la+1 { + d = make([]int, la+1) + } + d = d[:la+1] + + var lastdiag, olddiag, temp int + + for i := 1; i <= la; i++ { + d[i] = i + } + for i := 1; i <= lb; i++ { + d[0] = i + lastdiag = i - 1 + rowmin := max + 1 + for j := 1; j <= la; j++ { + olddiag = d[j] + min := d[j] + 1 + if (d[j-1] + 1) < min { + min = d[j-1] + 1 + } + if a[j-1] == b[i-1] { + temp = 0 + } else { + temp = 1 + } + if (lastdiag + temp) < min { + min = lastdiag + temp + } + if min < rowmin { + rowmin = min + } + d[j] = min + + lastdiag = olddiag + } + // after each row if rowmin isn't less than max stop + if rowmin > max { + return max, true, d + } + } + return d[la], false, d +} diff --git a/search/levenshtein_test.go b/search/levenshtein_test.go new file mode 100644 index 0000000..ef23980 --- /dev/null +++ b/search/levenshtein_test.go @@ -0,0 +1,126 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "testing" +) + +func TestLevenshteinDistance(t *testing.T) { + + tests := []struct { + a string + b string + dist int + }{ + { + "water", + "atec", + 2, + }, + { + "water", + "aphex", + 4, + }, + } + + for _, test := range tests { + actual := LevenshteinDistance(test.a, test.b) + if actual != test.dist { + t.Errorf("expected %d, got %d for %s and %s", test.dist, actual, test.a, test.b) + } + } +} + +func TestLevenshteinDistanceMax(t *testing.T) { + + tests := []struct { + a string + b string + max int + dist int + exceeded bool + }{ + { + a: "water", + b: "atec", + max: 1, + dist: 1, + exceeded: true, + }, + { + a: "water", + b: "christmas", + max: 3, + dist: 3, + exceeded: true, + }, + { + a: "", + b: "water", + max: 10, + dist: 5, + exceeded: false, + }, + { + a: "water", + b: "", + max: 3, + dist: 3, + exceeded: true, + }, + } + + for _, test := range tests { + actual, exceeded := LevenshteinDistanceMax(test.a, test.b, test.max) + if actual != test.dist || exceeded != test.exceeded { + t.Errorf("expected %d %t, got %d %t for %s and %s", test.dist, test.exceeded, actual, exceeded, test.a, test.b) + } + } +} + +// 5 terms that are less than 2 +// 5 terms that are more than 2 +var benchmarkTerms = []string{ + "watex", + "aters", + "wayer", + "wbter", + "yater", + "christmas", + "waterwaterwater", + "watcatdogfish", + "q", + "couchbase", +} + +func BenchmarkLevenshteinDistance(b *testing.B) { + a := "water" + for i := 0; i < b.N; i++ { + for _, t := range benchmarkTerms { + LevenshteinDistance(a, t) + } + } +} + +func BenchmarkLevenshteinDistanceMax(b *testing.B) { + a := "water" + for i := 0; i < b.N; i++ { + for _, t := range benchmarkTerms { + LevenshteinDistanceMax(a, t, 2) + } + } +} diff --git a/search/pool.go b/search/pool.go new file mode 100644 index 0000000..ba8be8f --- /dev/null +++ b/search/pool.go @@ -0,0 +1,91 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "reflect" +) + +var reflectStaticSizeDocumentMatchPool int + +func init() { + var dmp DocumentMatchPool + reflectStaticSizeDocumentMatchPool = int(reflect.TypeOf(dmp).Size()) +} + +// DocumentMatchPoolTooSmall is a callback function that can be executed +// when the DocumentMatchPool does not have sufficient capacity +// By default we just perform just-in-time allocation, but you could log +// a message, or panic, etc. +type DocumentMatchPoolTooSmall func(p *DocumentMatchPool) *DocumentMatch + +// DocumentMatchPool manages use/re-use of DocumentMatch instances +// it pre-allocates space from a single large block with the expected +// number of instances. It is not thread-safe as currently all +// aspects of search take place in a single goroutine. +type DocumentMatchPool struct { + avail DocumentMatchCollection + TooSmall DocumentMatchPoolTooSmall +} + +func defaultDocumentMatchPoolTooSmall(p *DocumentMatchPool) *DocumentMatch { + return &DocumentMatch{} +} + +// NewDocumentMatchPool will build a DocumentMatchPool with memory +// pre-allocated to accommodate the requested number of DocumentMatch +// instances +func NewDocumentMatchPool(size, sortsize int) *DocumentMatchPool { + avail := make(DocumentMatchCollection, size) + // pre-allocate the expected number of instances + startBlock := make([]DocumentMatch, size) + startSorts := make([]string, size*sortsize) + // make these initial instances available + i, j := 0, 0 + for i < size { + avail[i] = &startBlock[i] + avail[i].Sort = startSorts[j:j] + i += 1 + j += sortsize + } + return &DocumentMatchPool{ + avail: avail, + TooSmall: defaultDocumentMatchPoolTooSmall, + } +} + +// Get returns an available DocumentMatch from the pool +// if the pool was not allocated with sufficient size, an allocation will +// occur to satisfy this request. As a side-effect this will grow the size +// of the pool. +func (p *DocumentMatchPool) Get() *DocumentMatch { + var rv *DocumentMatch + if len(p.avail) > 0 { + rv, p.avail = p.avail[len(p.avail)-1], p.avail[:len(p.avail)-1] + } else { + rv = p.TooSmall(p) + } + return rv +} + +// Put returns a DocumentMatch to the pool +func (p *DocumentMatchPool) Put(d *DocumentMatch) { + if d == nil { + return + } + // reset DocumentMatch before returning it to available pool + d.Reset() + p.avail = append(p.avail, d) +} diff --git a/search/pool_test.go b/search/pool_test.go new file mode 100644 index 0000000..21df38b --- /dev/null +++ b/search/pool_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import "testing" + +func TestDocumentMatchPool(t *testing.T) { + + tooManyCalled := false + + // create a pool + dmp := NewDocumentMatchPool(10, 0) + dmp.TooSmall = func(inner *DocumentMatchPool) *DocumentMatch { + tooManyCalled = true + return &DocumentMatch{} + } + + // get 10 instances without returning + returned := make(DocumentMatchCollection, 10) + + for i := 0; i < 10; i++ { + returned[i] = dmp.Get() + if tooManyCalled { + t.Fatal("too many function called before expected") + } + } + + // get one more and see if too many function is called + extra := dmp.Get() + if !tooManyCalled { + t.Fatal("expected too many function to be called, but wasn't") + } + + // return the first 10 + for i := 0; i < 10; i++ { + dmp.Put(returned[i]) + } + + // check len and cap + if len(dmp.avail) != 10 { + t.Fatalf("expected 10 available, got %d", len(dmp.avail)) + } + if cap(dmp.avail) != 10 { + t.Fatalf("expected avail cap still 10, got %d", cap(dmp.avail)) + } + + // return the extra + dmp.Put(extra) + + // check len and cap grown to 11 + if len(dmp.avail) != 11 { + t.Fatalf("expected 11 available, got %d", len(dmp.avail)) + } + // cap grows, but not by 1 (append behavior) + if cap(dmp.avail) <= 10 { + t.Fatalf("expected avail cap mpore than 10, got %d", cap(dmp.avail)) + } +} diff --git a/search/query/bool_field.go b/search/query/bool_field.go new file mode 100644 index 0000000..5aa7bb8 --- /dev/null +++ b/search/query/bool_field.go @@ -0,0 +1,66 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type BoolFieldQuery struct { + Bool bool `json:"bool"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewBoolFieldQuery creates a new Query for boolean fields +func NewBoolFieldQuery(val bool) *BoolFieldQuery { + return &BoolFieldQuery{ + Bool: val, + } +} + +func (q *BoolFieldQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *BoolFieldQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *BoolFieldQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *BoolFieldQuery) Field() string { + return q.FieldVal +} + +func (q *BoolFieldQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + term := "F" + if q.Bool { + term = "T" + } + return searcher.NewTermSearcher(ctx, i, term, field, q.BoostVal.Value(), options) +} diff --git a/search/query/boolean.go b/search/query/boolean.go new file mode 100644 index 0000000..734dfd1 --- /dev/null +++ b/search/query/boolean.go @@ -0,0 +1,259 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type BooleanQuery struct { + Must Query `json:"must,omitempty"` + Should Query `json:"should,omitempty"` + MustNot Query `json:"must_not,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + queryStringMode bool +} + +// NewBooleanQuery creates a compound Query composed +// of several other Query objects. +// Result documents must satisfy ALL of the +// must Queries. +// Result documents must satisfy NONE of the must not +// Queries. +// Result documents that ALSO satisfy any of the should +// Queries will score higher. +func NewBooleanQuery(must []Query, should []Query, mustNot []Query) *BooleanQuery { + + rv := BooleanQuery{} + if len(must) > 0 { + rv.Must = NewConjunctionQuery(must) + } + if len(should) > 0 { + rv.Should = NewDisjunctionQuery(should) + } + if len(mustNot) > 0 { + rv.MustNot = NewDisjunctionQuery(mustNot) + } + + return &rv +} + +func NewBooleanQueryForQueryString(must []Query, should []Query, mustNot []Query) *BooleanQuery { + rv := NewBooleanQuery(nil, nil, nil) + rv.queryStringMode = true + rv.AddMust(must...) + rv.AddShould(should...) + rv.AddMustNot(mustNot...) + return rv +} + +// SetMinShould requires that at least minShould of the +// should Queries must be satisfied. +func (q *BooleanQuery) SetMinShould(minShould float64) { + q.Should.(*DisjunctionQuery).SetMin(minShould) +} + +func (q *BooleanQuery) AddMust(m ...Query) { + if m == nil { + return + } + if q.Must == nil { + tmp := NewConjunctionQuery([]Query{}) + tmp.queryStringMode = q.queryStringMode + q.Must = tmp + } + for _, mq := range m { + q.Must.(*ConjunctionQuery).AddQuery(mq) + } +} + +func (q *BooleanQuery) AddShould(m ...Query) { + if m == nil { + return + } + if q.Should == nil { + tmp := NewDisjunctionQuery([]Query{}) + tmp.queryStringMode = q.queryStringMode + q.Should = tmp + } + for _, mq := range m { + q.Should.(*DisjunctionQuery).AddQuery(mq) + } +} + +func (q *BooleanQuery) AddMustNot(m ...Query) { + if m == nil { + return + } + if q.MustNot == nil { + tmp := NewDisjunctionQuery([]Query{}) + tmp.queryStringMode = q.queryStringMode + q.MustNot = tmp + } + for _, mq := range m { + q.MustNot.(*DisjunctionQuery).AddQuery(mq) + } +} + +func (q *BooleanQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *BooleanQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *BooleanQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + var err error + var mustNotSearcher search.Searcher + if q.MustNot != nil { + mustNotSearcher, err = q.MustNot.Searcher(ctx, i, m, options) + if err != nil { + return nil, err + } + // if must not is MatchNone, reset it to nil + if _, ok := mustNotSearcher.(*searcher.MatchNoneSearcher); ok { + mustNotSearcher = nil + } + } + + var mustSearcher search.Searcher + if q.Must != nil { + mustSearcher, err = q.Must.Searcher(ctx, i, m, options) + if err != nil { + return nil, err + } + // if must searcher is MatchNone, reset it to nil + if _, ok := mustSearcher.(*searcher.MatchNoneSearcher); ok { + mustSearcher = nil + } + } + + var shouldSearcher search.Searcher + if q.Should != nil { + shouldSearcher, err = q.Should.Searcher(ctx, i, m, options) + if err != nil { + return nil, err + } + // if should searcher is MatchNone, reset it to nil + if _, ok := shouldSearcher.(*searcher.MatchNoneSearcher); ok { + shouldSearcher = nil + } + } + + // if all 3 are nil, return MatchNone + if mustSearcher == nil && shouldSearcher == nil && mustNotSearcher == nil { + return searcher.NewMatchNoneSearcher(i) + } + + // if only mustNotSearcher, start with MatchAll + if mustSearcher == nil && shouldSearcher == nil && mustNotSearcher != nil { + mustSearcher, err = searcher.NewMatchAllSearcher(ctx, i, 1.0, options) + if err != nil { + return nil, err + } + } + + // optimization, if only should searcher, just return it instead + if mustSearcher == nil && shouldSearcher != nil && mustNotSearcher == nil { + return shouldSearcher, nil + } + + return searcher.NewBooleanSearcher(ctx, i, mustSearcher, shouldSearcher, mustNotSearcher, options) +} + +func (q *BooleanQuery) Validate() error { + if qm, ok := q.Must.(ValidatableQuery); ok { + err := qm.Validate() + if err != nil { + return err + } + } + if qs, ok := q.Should.(ValidatableQuery); ok { + err := qs.Validate() + if err != nil { + return err + } + } + if qmn, ok := q.MustNot.(ValidatableQuery); ok { + err := qmn.Validate() + if err != nil { + return err + } + } + if q.Must == nil && q.Should == nil && q.MustNot == nil { + return fmt.Errorf("boolean query must contain at least one must or should or not must clause") + } + return nil +} + +func (q *BooleanQuery) UnmarshalJSON(data []byte) error { + tmp := struct { + Must json.RawMessage `json:"must,omitempty"` + Should json.RawMessage `json:"should,omitempty"` + MustNot json.RawMessage `json:"must_not,omitempty"` + Boost *Boost `json:"boost,omitempty"` + }{} + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + + if tmp.Must != nil { + q.Must, err = ParseQuery(tmp.Must) + if err != nil { + return err + } + _, isConjunctionQuery := q.Must.(*ConjunctionQuery) + if !isConjunctionQuery { + return fmt.Errorf("must clause must be conjunction") + } + } + + if tmp.Should != nil { + q.Should, err = ParseQuery(tmp.Should) + if err != nil { + return err + } + _, isDisjunctionQuery := q.Should.(*DisjunctionQuery) + if !isDisjunctionQuery { + return fmt.Errorf("should clause must be disjunction") + } + } + + if tmp.MustNot != nil { + q.MustNot, err = ParseQuery(tmp.MustNot) + if err != nil { + return err + } + _, isDisjunctionQuery := q.MustNot.(*DisjunctionQuery) + if !isDisjunctionQuery { + return fmt.Errorf("must not clause must be disjunction") + } + } + + q.BoostVal = tmp.Boost + + return nil +} diff --git a/search/query/boost.go b/search/query/boost.go new file mode 100644 index 0000000..1365994 --- /dev/null +++ b/search/query/boost.go @@ -0,0 +1,33 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import "fmt" + +type Boost float64 + +func (b *Boost) Value() float64 { + if b == nil { + return 1.0 + } + return float64(*b) +} + +func (b *Boost) GoString() string { + if b == nil { + return "boost unspecified" + } + return fmt.Sprintf("%f", *b) +} diff --git a/search/query/conjunction.go b/search/query/conjunction.go new file mode 100644 index 0000000..a204372 --- /dev/null +++ b/search/query/conjunction.go @@ -0,0 +1,112 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "encoding/json" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type ConjunctionQuery struct { + Conjuncts []Query `json:"conjuncts"` + BoostVal *Boost `json:"boost,omitempty"` + queryStringMode bool +} + +// NewConjunctionQuery creates a new compound Query. +// Result documents must satisfy all of the queries. +func NewConjunctionQuery(conjuncts []Query) *ConjunctionQuery { + return &ConjunctionQuery{ + Conjuncts: conjuncts, + } +} + +func (q *ConjunctionQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *ConjunctionQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *ConjunctionQuery) AddQuery(aq ...Query) { + q.Conjuncts = append(q.Conjuncts, aq...) +} + +func (q *ConjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + ss := make([]search.Searcher, 0, len(q.Conjuncts)) + for _, conjunct := range q.Conjuncts { + sr, err := conjunct.Searcher(ctx, i, m, options) + if err != nil { + for _, searcher := range ss { + if searcher != nil { + _ = searcher.Close() + } + } + return nil, err + } + if _, ok := sr.(*searcher.MatchNoneSearcher); ok && q.queryStringMode { + // in query string mode, skip match none + continue + } + ss = append(ss, sr) + } + + if len(ss) < 1 { + return searcher.NewMatchNoneSearcher(i) + } + + return searcher.NewConjunctionSearcher(ctx, i, ss, options) +} + +func (q *ConjunctionQuery) Validate() error { + for _, q := range q.Conjuncts { + if q, ok := q.(ValidatableQuery); ok { + err := q.Validate() + if err != nil { + return err + } + } + } + return nil +} + +func (q *ConjunctionQuery) UnmarshalJSON(data []byte) error { + tmp := struct { + Conjuncts []json.RawMessage `json:"conjuncts"` + Boost *Boost `json:"boost,omitempty"` + }{} + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + q.Conjuncts = make([]Query, len(tmp.Conjuncts)) + for i, term := range tmp.Conjuncts { + query, err := ParseQuery(term) + if err != nil { + return err + } + q.Conjuncts[i] = query + } + q.BoostVal = tmp.Boost + return nil +} diff --git a/search/query/date_range.go b/search/query/date_range.go new file mode 100644 index 0000000..47012fb --- /dev/null +++ b/search/query/date_range.go @@ -0,0 +1,192 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/blevesearch/bleve/v2/analysis/datetime/optional" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/registry" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +// QueryDateTimeParser controls the default query date time parser. +var QueryDateTimeParser = optional.Name + +// QueryDateTimeFormat controls the format when Marshaling to JSON. +var QueryDateTimeFormat = time.RFC3339 + +var cache = registry.NewCache() + +type BleveQueryTime struct { + time.Time +} + +var MinRFC3339CompatibleTime time.Time +var MaxRFC3339CompatibleTime time.Time + +func init() { + MinRFC3339CompatibleTime, _ = time.Parse(time.RFC3339, "1677-12-01T00:00:00Z") + MaxRFC3339CompatibleTime, _ = time.Parse(time.RFC3339, "2262-04-11T11:59:59Z") +} + +func queryTimeFromString(t string) (time.Time, error) { + dateTimeParser, err := cache.DateTimeParserNamed(QueryDateTimeParser) + if err != nil { + return time.Time{}, err + } + rv, _, err := dateTimeParser.ParseDateTime(t) + if err != nil { + return time.Time{}, err + } + return rv, nil +} + +func (t *BleveQueryTime) MarshalJSON() ([]byte, error) { + tt := time.Time(t.Time) + return []byte("\"" + tt.Format(QueryDateTimeFormat) + "\""), nil +} + +func (t *BleveQueryTime) UnmarshalJSON(data []byte) error { + var timeString string + err := util.UnmarshalJSON(data, &timeString) + if err != nil { + return err + } + dateTimeParser, err := cache.DateTimeParserNamed(QueryDateTimeParser) + if err != nil { + return err + } + t.Time, _, err = dateTimeParser.ParseDateTime(timeString) + if err != nil { + return err + } + return nil +} + +type DateRangeQuery struct { + Start BleveQueryTime `json:"start,omitempty"` + End BleveQueryTime `json:"end,omitempty"` + InclusiveStart *bool `json:"inclusive_start,omitempty"` + InclusiveEnd *bool `json:"inclusive_end,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewDateRangeQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser configured in the +// top-level config.QueryDateTimeParser +// Either, but not both endpoints can be nil. +func NewDateRangeQuery(start, end time.Time) *DateRangeQuery { + return NewDateRangeInclusiveQuery(start, end, nil, nil) +} + +// NewDateRangeInclusiveQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser configured in the +// top-level config.QueryDateTimeParser +// Either, but not both endpoints can be nil. +// startInclusive and endInclusive control inclusion of the endpoints. +func NewDateRangeInclusiveQuery(start, end time.Time, startInclusive, endInclusive *bool) *DateRangeQuery { + return &DateRangeQuery{ + Start: BleveQueryTime{start}, + End: BleveQueryTime{end}, + InclusiveStart: startInclusive, + InclusiveEnd: endInclusive, + } +} + +func (q *DateRangeQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *DateRangeQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *DateRangeQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *DateRangeQuery) Field() string { + return q.FieldVal +} + +func (q *DateRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + min, max, err := q.parseEndpoints() + if err != nil { + return nil, err + } + + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + return searcher.NewNumericRangeSearcher(ctx, i, min, max, q.InclusiveStart, q.InclusiveEnd, field, q.BoostVal.Value(), options) +} + +func (q *DateRangeQuery) parseEndpoints() (*float64, *float64, error) { + min := math.Inf(-1) + max := math.Inf(1) + if !q.Start.IsZero() { + if !isDatetimeCompatible(q.Start) { + // overflow + return nil, nil, fmt.Errorf("invalid/unsupported date range, start: %v", q.Start) + } + startInt64 := q.Start.UnixNano() + min = numeric.Int64ToFloat64(startInt64) + } + if !q.End.IsZero() { + if !isDatetimeCompatible(q.End) { + // overflow + return nil, nil, fmt.Errorf("invalid/unsupported date range, end: %v", q.End) + } + endInt64 := q.End.UnixNano() + max = numeric.Int64ToFloat64(endInt64) + } + + return &min, &max, nil +} + +func (q *DateRangeQuery) Validate() error { + if q.Start.IsZero() && q.End.IsZero() { + return fmt.Errorf("must specify start or end") + } + _, _, err := q.parseEndpoints() + if err != nil { + return err + } + return nil +} + +func isDatetimeCompatible(t BleveQueryTime) bool { + if QueryDateTimeFormat == time.RFC3339 && + (t.Before(MinRFC3339CompatibleTime) || t.After(MaxRFC3339CompatibleTime)) { + return false + } + + return true +} diff --git a/search/query/date_range_string.go b/search/query/date_range_string.go new file mode 100644 index 0000000..ac10719 --- /dev/null +++ b/search/query/date_range_string.go @@ -0,0 +1,176 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +// DateRangeStringQuery represents a query for a range of date values. +// Start and End are the range endpoints, as strings. +// Start and End are parsed using DateTimeParser, which is a custom date time parser +// defined in the index mapping. If DateTimeParser is not specified, then the +// top-level config.QueryDateTimeParser is used. +type DateRangeStringQuery struct { + Start string `json:"start,omitempty"` + End string `json:"end,omitempty"` + InclusiveStart *bool `json:"inclusive_start,omitempty"` + InclusiveEnd *bool `json:"inclusive_end,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + DateTimeParser string `json:"datetime_parser,omitempty"` +} + +// NewDateRangeStringQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser field of the query struct, +// which is a custom date time parser defined in the index mapping. +// if DateTimeParser is not specified, then the +// top-level config.QueryDateTimeParser is used. +// Either, but not both endpoints can be nil. +func NewDateRangeStringQuery(start, end string) *DateRangeStringQuery { + return NewDateRangeStringInclusiveQuery(start, end, nil, nil) +} + +// NewDateRangeStringInclusiveQuery creates a new Query for ranges +// of date values. +// Date strings are parsed using the DateTimeParser field of the query struct, +// which is a custom date time parser defined in the index mapping. +// if DateTimeParser is not specified, then the +// top-level config.QueryDateTimeParser is used. +// Either, but not both endpoints can be nil. +// startInclusive and endInclusive control inclusion of the endpoints. +func NewDateRangeStringInclusiveQuery(start, end string, startInclusive, endInclusive *bool) *DateRangeStringQuery { + return &DateRangeStringQuery{ + Start: start, + End: end, + InclusiveStart: startInclusive, + InclusiveEnd: endInclusive, + } +} + +func (q *DateRangeStringQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *DateRangeStringQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *DateRangeStringQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *DateRangeStringQuery) Field() string { + return q.FieldVal +} + +func (q *DateRangeStringQuery) SetDateTimeParser(d string) { + q.DateTimeParser = d +} + +func (q *DateRangeStringQuery) DateTimeParserName() string { + return q.DateTimeParser +} + +func (q *DateRangeStringQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + dateTimeParserName := QueryDateTimeParser + if q.DateTimeParser != "" { + dateTimeParserName = q.DateTimeParser + } + dateTimeParser := m.DateTimeParserNamed(dateTimeParserName) + if dateTimeParser == nil { + return nil, fmt.Errorf("no dateTimeParser named '%s' registered", dateTimeParserName) + } + + var startTime, endTime time.Time + var err error + if q.Start != "" { + startTime, _, err = dateTimeParser.ParseDateTime(q.Start) + if err != nil { + return nil, fmt.Errorf("%v, date time parser name: %s", err, dateTimeParserName) + } + } + if q.End != "" { + endTime, _, err = dateTimeParser.ParseDateTime(q.End) + if err != nil { + return nil, fmt.Errorf("%v, date time parser name: %s", err, dateTimeParserName) + } + } + + min, max, err := q.parseEndpoints(startTime, endTime) + if err != nil { + return nil, err + } + return searcher.NewNumericRangeSearcher(ctx, i, min, max, q.InclusiveStart, q.InclusiveEnd, field, q.BoostVal.Value(), options) +} + +func (q *DateRangeStringQuery) parseEndpoints(startTime, endTime time.Time) (*float64, *float64, error) { + min := math.Inf(-1) + max := math.Inf(1) + + if startTime.IsZero() && endTime.IsZero() { + return nil, nil, fmt.Errorf("date range query must specify at least one of start/end") + } + + if !startTime.IsZero() { + if !isDateTimeWithinRange(startTime) { + // overflow + return nil, nil, fmt.Errorf("invalid/unsupported date range, start: %v", q.Start) + } + startInt64 := startTime.UnixNano() + min = numeric.Int64ToFloat64(startInt64) + } + if !endTime.IsZero() { + if !isDateTimeWithinRange(endTime) { + // overflow + return nil, nil, fmt.Errorf("invalid/unsupported date range, end: %v", q.End) + } + endInt64 := endTime.UnixNano() + max = numeric.Int64ToFloat64(endInt64) + } + + return &min, &max, nil +} + +func (q *DateRangeStringQuery) Validate() error { + // either start or end must be specified + if q.Start == "" && q.End == "" { + return fmt.Errorf("date range query must specify at least one of start/end") + } + return nil +} + +func isDateTimeWithinRange(t time.Time) bool { + if t.Before(MinRFC3339CompatibleTime) || t.After(MaxRFC3339CompatibleTime) { + return false + } + return true +} diff --git a/search/query/date_range_test.go b/search/query/date_range_test.go new file mode 100644 index 0000000..bbf8280 --- /dev/null +++ b/search/query/date_range_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "encoding/json" + "testing" + "time" +) + +func TestBleveQueryTime(t *testing.T) { + testTimes := []time.Time{ + time.Now(), + {}, + } + + for i, testTime := range testTimes { + bqt := &BleveQueryTime{testTime} + + buf, err := json.Marshal(bqt) + if err != nil { + t.Errorf("expected no err") + } + + var bqt2 BleveQueryTime + err = json.Unmarshal(buf, &bqt2) + if err != nil { + t.Errorf("expected no unmarshal err, got: %v", err) + } + + if bqt.Time.Format(time.RFC3339) != bqt2.Time.Format(time.RFC3339) { + t.Errorf("test %d - expected same time, %#v != %#v", i, bqt.Time, bqt2.Time) + } + + if testTime.Format(time.RFC3339) != bqt2.Time.Format(time.RFC3339) { + t.Errorf("test %d - expected orig time, %#v != %#v", i, testTime, bqt2.Time) + } + } +} + +func TestValidateDatetimeRanges(t *testing.T) { + tests := []struct { + start string + end string + expect bool + }{ + { + start: "2019-03-22T13:25:00Z", + end: "2019-03-22T18:25:00Z", + expect: true, + }, + { + start: "2019-03-22T13:25:00Z", + end: "9999-03-22T13:25:00Z", + expect: false, + }, + { + start: "2019-03-22T13:25:00Z", + end: "2262-04-11T11:59:59Z", + expect: true, + }, + { + start: "2019-03-22T13:25:00Z", + end: "2262-04-12T00:00:00Z", + expect: false, + }, + { + start: "1950-03-22T12:23:23Z", + end: "1960-02-21T15:23:34Z", + expect: true, + }, + { + start: "0001-01-01T00:00:00Z", + end: "0001-01-01T00:00:00Z", + expect: false, + }, + { + start: "0001-01-01T00:00:00Z", + end: "2000-01-01T00:00:00Z", + expect: true, + }, + { + start: "1677-11-30T11:59:59Z", + end: "2262-04-11T11:59:59Z", + expect: false, + }, + { + start: "2262-04-12T00:00:00Z", + end: "2262-04-11T11:59:59Z", + expect: false, + }, + { + start: "1677-12-01T00:00:00Z", + end: "2262-04-12T00:00:00Z", + expect: false, + }, + { + start: "1677-12-01T00:00:00Z", + end: "1677-11-30T11:59:59Z", + expect: false, + }, + { + start: "1677-12-01T00:00:00Z", + end: "2262-04-11T11:59:59Z", + expect: true, + }, + } + + for _, test := range tests { + startTime, _ := time.Parse(time.RFC3339, test.start) + endTime, _ := time.Parse(time.RFC3339, test.end) + + dateRangeQuery := NewDateRangeQuery(startTime, endTime) + if (dateRangeQuery.Validate() == nil) != test.expect { + t.Errorf("unexpected results while validating date range query with"+ + " {start: %v, end: %v}, expected: %v", + test.start, test.end, test.expect) + } + } +} diff --git a/search/query/disjunction.go b/search/query/disjunction.go new file mode 100644 index 0000000..da46478 --- /dev/null +++ b/search/query/disjunction.go @@ -0,0 +1,134 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type DisjunctionQuery struct { + Disjuncts []Query `json:"disjuncts"` + BoostVal *Boost `json:"boost,omitempty"` + Min float64 `json:"min"` + retrieveScoreBreakdown bool + queryStringMode bool +} + +func (q *DisjunctionQuery) RetrieveScoreBreakdown(b bool) { + q.retrieveScoreBreakdown = b +} + +// NewDisjunctionQuery creates a new compound Query. +// Result documents satisfy at least one Query. +func NewDisjunctionQuery(disjuncts []Query) *DisjunctionQuery { + return &DisjunctionQuery{ + Disjuncts: disjuncts, + } +} + +func (q *DisjunctionQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *DisjunctionQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *DisjunctionQuery) AddQuery(aq ...Query) { + q.Disjuncts = append(q.Disjuncts, aq...) +} + +func (q *DisjunctionQuery) SetMin(m float64) { + q.Min = m +} + +func (q *DisjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, + options search.SearcherOptions, +) (search.Searcher, error) { + ss := make([]search.Searcher, 0, len(q.Disjuncts)) + for _, disjunct := range q.Disjuncts { + sr, err := disjunct.Searcher(ctx, i, m, options) + if err != nil { + for _, searcher := range ss { + if searcher != nil { + _ = searcher.Close() + } + } + return nil, err + } + if sr != nil { + if _, ok := sr.(*searcher.MatchNoneSearcher); ok && q.queryStringMode { + // in query string mode, skip match none + continue + } + ss = append(ss, sr) + } + } + + if len(ss) < 1 { + return searcher.NewMatchNoneSearcher(i) + } + + nctx := context.WithValue(ctx, search.IncludeScoreBreakdownKey, q.retrieveScoreBreakdown) + + return searcher.NewDisjunctionSearcher(nctx, i, ss, q.Min, options) +} + +func (q *DisjunctionQuery) Validate() error { + if int(q.Min) > len(q.Disjuncts) { + return fmt.Errorf("disjunction query has fewer than the minimum number of clauses to satisfy") + } + for _, q := range q.Disjuncts { + if q, ok := q.(ValidatableQuery); ok { + err := q.Validate() + if err != nil { + return err + } + } + } + return nil +} + +func (q *DisjunctionQuery) UnmarshalJSON(data []byte) error { + tmp := struct { + Disjuncts []json.RawMessage `json:"disjuncts"` + Boost *Boost `json:"boost,omitempty"` + Min float64 `json:"min"` + }{} + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + q.Disjuncts = make([]Query, len(tmp.Disjuncts)) + for i, term := range tmp.Disjuncts { + query, err := ParseQuery(term) + if err != nil { + return err + } + q.Disjuncts[i] = query + } + q.BoostVal = tmp.Boost + q.Min = tmp.Min + return nil +} diff --git a/search/query/docid.go b/search/query/docid.go new file mode 100644 index 0000000..7116f39 --- /dev/null +++ b/search/query/docid.go @@ -0,0 +1,51 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type DocIDQuery struct { + IDs []string `json:"ids"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewDocIDQuery creates a new Query object returning indexed documents among +// the specified set. Combine it with ConjunctionQuery to restrict the scope of +// other queries output. +func NewDocIDQuery(ids []string) *DocIDQuery { + return &DocIDQuery{ + IDs: ids, + } +} + +func (q *DocIDQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *DocIDQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *DocIDQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + return searcher.NewDocIDSearcher(ctx, i, q.IDs, q.BoostVal.Value(), options) +} diff --git a/search/query/fuzzy.go b/search/query/fuzzy.go new file mode 100644 index 0000000..72d7c0e --- /dev/null +++ b/search/query/fuzzy.go @@ -0,0 +1,134 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type FuzzyQuery struct { + Term string `json:"term"` + Prefix int `json:"prefix_length"` + Fuzziness int `json:"fuzziness"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + autoFuzzy bool +} + +// NewFuzzyQuery creates a new Query which finds +// documents containing terms within a specific +// fuzziness of the specified term. +// The default fuzziness is 1. +// +// The current implementation uses Levenshtein edit +// distance as the fuzziness metric. +func NewFuzzyQuery(term string) *FuzzyQuery { + return &FuzzyQuery{ + Term: term, + Fuzziness: 1, + } +} + +func (q *FuzzyQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *FuzzyQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *FuzzyQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *FuzzyQuery) Field() string { + return q.FieldVal +} + +func (q *FuzzyQuery) SetFuzziness(f int) { + q.Fuzziness = f +} + +func (q *FuzzyQuery) SetAutoFuzziness(a bool) { + q.autoFuzzy = a +} + +func (q *FuzzyQuery) SetPrefix(p int) { + q.Prefix = p +} + +func (q *FuzzyQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + if q.autoFuzzy { + return searcher.NewAutoFuzzySearcher(ctx, i, q.Term, q.Prefix, field, q.BoostVal.Value(), options) + } + return searcher.NewFuzzySearcher(ctx, i, q.Term, q.Prefix, q.Fuzziness, field, q.BoostVal.Value(), options) +} + +func (q *FuzzyQuery) UnmarshalJSON(data []byte) error { + type Alias FuzzyQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +func (f *FuzzyQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type fuzzyQuery struct { + Term string `json:"term"` + Prefix int `json:"prefix_length"` + Fuzziness interface{} `json:"fuzziness"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + } + aux := fuzzyQuery{ + Term: f.Term, + Prefix: f.Prefix, + Fuzziness: fuzzyValue, + FieldVal: f.FieldVal, + BoostVal: f.BoostVal, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/geo_boundingbox.go b/search/query/geo_boundingbox.go new file mode 100644 index 0000000..1653e6e --- /dev/null +++ b/search/query/geo_boundingbox.go @@ -0,0 +1,119 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type GeoBoundingBoxQuery struct { + TopLeft []float64 `json:"top_left,omitempty"` + BottomRight []float64 `json:"bottom_right,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +func NewGeoBoundingBoxQuery(topLeftLon, topLeftLat, bottomRightLon, bottomRightLat float64) *GeoBoundingBoxQuery { + return &GeoBoundingBoxQuery{ + TopLeft: []float64{topLeftLon, topLeftLat}, + BottomRight: []float64{bottomRightLon, bottomRightLat}, + } +} + +func (q *GeoBoundingBoxQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *GeoBoundingBoxQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *GeoBoundingBoxQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *GeoBoundingBoxQuery) Field() string { + return q.FieldVal +} + +func (q *GeoBoundingBoxQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + ctx = context.WithValue(ctx, search.QueryTypeKey, search.Geo) + + if q.BottomRight[0] < q.TopLeft[0] { + // cross date line, rewrite as two parts + + leftSearcher, err := searcher.NewGeoBoundingBoxSearcher(ctx, i, -180, q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true) + if err != nil { + return nil, err + } + rightSearcher, err := searcher.NewGeoBoundingBoxSearcher(ctx, i, q.TopLeft[0], q.BottomRight[1], 180, q.TopLeft[1], field, q.BoostVal.Value(), options, true) + if err != nil { + _ = leftSearcher.Close() + return nil, err + } + + return searcher.NewDisjunctionSearcher(ctx, i, []search.Searcher{leftSearcher, rightSearcher}, 0, options) + } + + return searcher.NewGeoBoundingBoxSearcher(ctx, i, q.TopLeft[0], q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true) +} + +func (q *GeoBoundingBoxQuery) Validate() error { + if q.TopLeft[1] < q.BottomRight[1] { + return fmt.Errorf("geo bounding box top left should be higher than bottom right") + } + return nil +} + +func (q *GeoBoundingBoxQuery) UnmarshalJSON(data []byte) error { + tmp := struct { + TopLeft interface{} `json:"top_left,omitempty"` + BottomRight interface{} `json:"bottom_right,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + }{} + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + // now use our generic point parsing code from the geo package + lon, lat, found := geo.ExtractGeoPoint(tmp.TopLeft) + if !found { + return fmt.Errorf("geo location top_left not in a valid format") + } + q.TopLeft = []float64{lon, lat} + lon, lat, found = geo.ExtractGeoPoint(tmp.BottomRight) + if !found { + return fmt.Errorf("geo location bottom_right not in a valid format") + } + q.BottomRight = []float64{lon, lat} + q.FieldVal = tmp.FieldVal + q.BoostVal = tmp.BoostVal + return nil +} diff --git a/search/query/geo_boundingpolygon.go b/search/query/geo_boundingpolygon.go new file mode 100644 index 0000000..7f81a7c --- /dev/null +++ b/search/query/geo_boundingpolygon.go @@ -0,0 +1,97 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type GeoBoundingPolygonQuery struct { + Points []geo.Point `json:"polygon_points"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +func NewGeoBoundingPolygonQuery(points []geo.Point) *GeoBoundingPolygonQuery { + return &GeoBoundingPolygonQuery{ + Points: points} +} + +func (q *GeoBoundingPolygonQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *GeoBoundingPolygonQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *GeoBoundingPolygonQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *GeoBoundingPolygonQuery) Field() string { + return q.FieldVal +} + +func (q *GeoBoundingPolygonQuery) Searcher(ctx context.Context, i index.IndexReader, + m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + ctx = context.WithValue(ctx, search.QueryTypeKey, search.Geo) + + return searcher.NewGeoBoundedPolygonSearcher(ctx, i, q.Points, field, q.BoostVal.Value(), options) +} + +func (q *GeoBoundingPolygonQuery) Validate() error { + return nil +} + +func (q *GeoBoundingPolygonQuery) UnmarshalJSON(data []byte) error { + tmp := struct { + Points []interface{} `json:"polygon_points"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + }{} + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + + q.Points = make([]geo.Point, 0, len(tmp.Points)) + for _, i := range tmp.Points { + // now use our generic point parsing code from the geo package + lon, lat, found := geo.ExtractGeoPoint(i) + if !found { + return fmt.Errorf("geo polygon point: %v is not in a valid format", i) + } + q.Points = append(q.Points, geo.Point{Lon: lon, Lat: lat}) + } + + q.FieldVal = tmp.FieldVal + q.BoostVal = tmp.BoostVal + return nil +} diff --git a/search/query/geo_distance.go b/search/query/geo_distance.go new file mode 100644 index 0000000..2ca0964 --- /dev/null +++ b/search/query/geo_distance.go @@ -0,0 +1,103 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type GeoDistanceQuery struct { + Location []float64 `json:"location,omitempty"` + Distance string `json:"distance,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +func NewGeoDistanceQuery(lon, lat float64, distance string) *GeoDistanceQuery { + return &GeoDistanceQuery{ + Location: []float64{lon, lat}, + Distance: distance, + } +} + +func (q *GeoDistanceQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *GeoDistanceQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *GeoDistanceQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *GeoDistanceQuery) Field() string { + return q.FieldVal +} + +func (q *GeoDistanceQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, + options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + ctx = context.WithValue(ctx, search.QueryTypeKey, search.Geo) + + dist, err := geo.ParseDistance(q.Distance) + if err != nil { + return nil, err + } + + return searcher.NewGeoPointDistanceSearcher(ctx, i, q.Location[0], q.Location[1], + dist, field, q.BoostVal.Value(), options) +} + +func (q *GeoDistanceQuery) Validate() error { + return nil +} + +func (q *GeoDistanceQuery) UnmarshalJSON(data []byte) error { + tmp := struct { + Location interface{} `json:"location,omitempty"` + Distance string `json:"distance,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + }{} + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + // now use our generic point parsing code from the geo package + lon, lat, found := geo.ExtractGeoPoint(tmp.Location) + if !found { + return fmt.Errorf("geo location not in a valid format") + } + q.Location = []float64{lon, lat} + q.Distance = tmp.Distance + q.FieldVal = tmp.FieldVal + q.BoostVal = tmp.BoostVal + return nil +} diff --git a/search/query/geo_shape.go b/search/query/geo_shape.go new file mode 100644 index 0000000..686f486 --- /dev/null +++ b/search/query/geo_shape.go @@ -0,0 +1,138 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "encoding/json" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type Geometry struct { + Shape index.GeoJSON `json:"shape"` + Relation string `json:"relation"` +} + +type GeoShapeQuery struct { + Geometry Geometry `json:"geometry"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewGeoShapeQuery creates a geoshape query for the +// given shape type. This method can be used for +// creating geoshape queries for shape types like: point, +// linestring, polygon, multipoint, multilinestring, +// multipolygon and envelope. +func NewGeoShapeQuery(coordinates [][][][]float64, typ, + relation string) (*GeoShapeQuery, error) { + s, _, err := geo.NewGeoJsonShape(coordinates, typ) + if err != nil { + return nil, err + } + + return &GeoShapeQuery{Geometry: Geometry{Shape: s, + Relation: relation}}, nil +} + +// NewGeoShapeCircleQuery creates a geoshape query for the +// given center point and the radius. Radius formats supported: +// "5in" "5inch" "7yd" "7yards" "9ft" "9feet" "11km" "11kilometers" +// "3nm" "3nauticalmiles" "13mm" "13millimeters" "15cm" "15centimeters" +// "17mi" "17miles" "19m" "19meters" If the unit cannot be determined, +// the entire string is parsed and the unit of meters is assumed. +func NewGeoShapeCircleQuery(coordinates []float64, radius, + relation string) (*GeoShapeQuery, error) { + + s, _, err := geo.NewGeoCircleShape(coordinates, radius) + if err != nil { + return nil, err + } + + return &GeoShapeQuery{Geometry: Geometry{Shape: s, + Relation: relation}}, nil +} + +// NewGeometryCollectionQuery creates a geoshape query for the +// given geometrycollection coordinates and types. +func NewGeometryCollectionQuery(coordinates [][][][][]float64, types []string, + relation string) (*GeoShapeQuery, error) { + s, _, err := geo.NewGeometryCollection(coordinates, types) + if err != nil { + return nil, err + } + + return &GeoShapeQuery{Geometry: Geometry{Shape: s, + Relation: relation}}, nil +} + +func (q *GeoShapeQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *GeoShapeQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *GeoShapeQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *GeoShapeQuery) Field() string { + return q.FieldVal +} + +func (q *GeoShapeQuery) Searcher(ctx context.Context, i index.IndexReader, + m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + ctx = context.WithValue(ctx, search.QueryTypeKey, search.Geo) + + return searcher.NewGeoShapeSearcher(ctx, i, q.Geometry.Shape, q.Geometry.Relation, field, + q.BoostVal.Value(), options) +} + +func (q *GeoShapeQuery) Validate() error { + return nil +} + +func (q *Geometry) UnmarshalJSON(data []byte) error { + tmp := struct { + Shape json.RawMessage `json:"shape"` + Relation string `json:"relation"` + }{} + + err := util.UnmarshalJSON(data, &tmp) + if err != nil { + return err + } + + q.Shape, err = geo.ParseGeoJSONShape(tmp.Shape) + if err != nil { + return err + } + q.Relation = tmp.Relation + return nil +} diff --git a/search/query/ip_range.go b/search/query/ip_range.go new file mode 100644 index 0000000..6c447c2 --- /dev/null +++ b/search/query/ip_range.go @@ -0,0 +1,85 @@ +// Copyright (c) 2021 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + "net" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type IPRangeQuery struct { + CIDR string `json:"cidr,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +func NewIPRangeQuery(cidr string) *IPRangeQuery { + return &IPRangeQuery{ + CIDR: cidr, + } +} + +func (q *IPRangeQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *IPRangeQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *IPRangeQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *IPRangeQuery) Field() string { + return q.FieldVal +} + +func (q *IPRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + _, ipNet, err := net.ParseCIDR(q.CIDR) + if err != nil { + ip := net.ParseIP(q.CIDR) + if ip == nil { + return nil, err + } + // If we are searching for a specific ip rather than members of a network, just use a term search. + return searcher.NewTermSearcherBytes(ctx, i, ip.To16(), field, q.BoostVal.Value(), options) + } + return searcher.NewIPRangeSearcher(ctx, i, ipNet, field, q.BoostVal.Value(), options) +} + +func (q *IPRangeQuery) Validate() error { + _, _, err := net.ParseCIDR(q.CIDR) + if err == nil { + return nil + } + // We also allow search for a specific IP. + ip := net.ParseIP(q.CIDR) + if ip != nil { + return nil // we have a valid ip + } + return fmt.Errorf("IPRangeQuery must be for a network or ip address, %q", q.CIDR) +} diff --git a/search/query/knn.go b/search/query/knn.go new file mode 100644 index 0000000..ea3d38c --- /dev/null +++ b/search/query/knn.go @@ -0,0 +1,95 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package query + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type KNNQuery struct { + VectorField string `json:"field"` + Vector []float32 `json:"vector"` + K int64 `json:"k"` + BoostVal *Boost `json:"boost,omitempty"` + + // see KNNRequest.Params for description + Params json.RawMessage `json:"params"` + // elegibleSelector is used to filter out documents that are + // eligible for the KNN search from a pre-filter query. + elegibleSelector index.EligibleDocumentSelector +} + +func NewKNNQuery(vector []float32) *KNNQuery { + return &KNNQuery{Vector: vector} +} + +func (q *KNNQuery) Field() string { + return q.VectorField +} + +func (q *KNNQuery) SetK(k int64) { + q.K = k +} + +func (q *KNNQuery) SetFieldVal(field string) { + q.VectorField = field +} + +func (q *KNNQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *KNNQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *KNNQuery) SetParams(params json.RawMessage) { + q.Params = params +} + +func (q *KNNQuery) SetEligibleSelector(eligibleSelector index.EligibleDocumentSelector) { + q.elegibleSelector = eligibleSelector +} + +func (q *KNNQuery) Searcher(ctx context.Context, i index.IndexReader, + m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + fieldMapping := m.FieldMappingForPath(q.VectorField) + similarityMetric := fieldMapping.Similarity + if similarityMetric == "" { + similarityMetric = index.DefaultVectorSimilarityMetric + } + if q.K <= 0 || len(q.Vector) == 0 { + return nil, fmt.Errorf("k must be greater than 0 and vector must be non-empty") + } + if similarityMetric == index.CosineSimilarity { + // normalize the vector + q.Vector = mapping.NormalizeVector(q.Vector) + } + + return searcher.NewKNNSearcher(ctx, i, m, options, q.VectorField, + q.Vector, q.K, q.BoostVal.Value(), similarityMetric, q.Params, + q.elegibleSelector) +} diff --git a/search/query/match.go b/search/query/match.go new file mode 100644 index 0000000..ba84d92 --- /dev/null +++ b/search/query/match.go @@ -0,0 +1,236 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type MatchQuery struct { + Match string `json:"match"` + FieldVal string `json:"field,omitempty"` + Analyzer string `json:"analyzer,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Prefix int `json:"prefix_length"` + Fuzziness int `json:"fuzziness"` + Operator MatchQueryOperator `json:"operator,omitempty"` + autoFuzzy bool +} + +type MatchQueryOperator int + +const ( + // Document must satisfy AT LEAST ONE of term searches. + MatchQueryOperatorOr = MatchQueryOperator(0) + // Document must satisfy ALL of term searches. + MatchQueryOperatorAnd = MatchQueryOperator(1) +) + +func (o MatchQueryOperator) MarshalJSON() ([]byte, error) { + switch o { + case MatchQueryOperatorOr: + return util.MarshalJSON("or") + case MatchQueryOperatorAnd: + return util.MarshalJSON("and") + default: + return nil, fmt.Errorf("cannot marshal match operator %d to JSON", o) + } +} + +func (o *MatchQueryOperator) UnmarshalJSON(data []byte) error { + var operatorString string + err := util.UnmarshalJSON(data, &operatorString) + if err != nil { + return err + } + + switch operatorString { + case "or": + *o = MatchQueryOperatorOr + return nil + case "and": + *o = MatchQueryOperatorAnd + return nil + default: + return fmt.Errorf("cannot unmarshal match operator '%v' from JSON", o) + } +} + +// NewMatchQuery creates a Query for matching text. +// An Analyzer is chosen based on the field. +// Input text is analyzed using this analyzer. +// Token terms resulting from this analysis are +// used to perform term searches. Result documents +// must satisfy at least one of these term searches. +func NewMatchQuery(match string) *MatchQuery { + return &MatchQuery{ + Match: match, + Operator: MatchQueryOperatorOr, + } +} + +func (q *MatchQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *MatchQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *MatchQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *MatchQuery) Field() string { + return q.FieldVal +} + +func (q *MatchQuery) SetFuzziness(f int) { + q.Fuzziness = f +} + +func (q *MatchQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + +func (q *MatchQuery) SetPrefix(p int) { + q.Prefix = p +} + +func (q *MatchQuery) SetOperator(operator MatchQueryOperator) { + q.Operator = operator +} + +func (q *MatchQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + analyzerName := "" + if q.Analyzer != "" { + analyzerName = q.Analyzer + } else { + analyzerName = m.AnalyzerNameForPath(field) + } + analyzer := m.AnalyzerNamed(analyzerName) + + if analyzer == nil { + return nil, fmt.Errorf("no analyzer named '%s' registered", q.Analyzer) + } + + tokens := analyzer.Analyze([]byte(q.Match)) + if len(tokens) > 0 { + + tqs := make([]Query, len(tokens)) + if q.Fuzziness != 0 || q.autoFuzzy { + for i, token := range tokens { + query := NewFuzzyQuery(string(token.Term)) + if q.autoFuzzy { + query.SetAutoFuzziness(true) + } else { + query.SetFuzziness(q.Fuzziness) + } + query.SetPrefix(q.Prefix) + query.SetField(field) + query.SetBoost(q.BoostVal.Value()) + tqs[i] = query + } + } else { + for i, token := range tokens { + tq := NewTermQuery(string(token.Term)) + tq.SetField(field) + tq.SetBoost(q.BoostVal.Value()) + tqs[i] = tq + } + } + + switch q.Operator { + case MatchQueryOperatorOr: + shouldQuery := NewDisjunctionQuery(tqs) + shouldQuery.SetMin(1) + shouldQuery.SetBoost(q.BoostVal.Value()) + return shouldQuery.Searcher(ctx, i, m, options) + + case MatchQueryOperatorAnd: + mustQuery := NewConjunctionQuery(tqs) + mustQuery.SetBoost(q.BoostVal.Value()) + return mustQuery.Searcher(ctx, i, m, options) + + default: + return nil, fmt.Errorf("unhandled operator %d", q.Operator) + } + } + noneQuery := NewMatchNoneQuery() + return noneQuery.Searcher(ctx, i, m, options) +} + +func (q *MatchQuery) UnmarshalJSON(data []byte) error { + type Alias MatchQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +func (f *MatchQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type match struct { + Match string `json:"match"` + FieldVal string `json:"field,omitempty"` + Analyzer string `json:"analyzer,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Prefix int `json:"prefix_length"` + Fuzziness interface{} `json:"fuzziness"` + Operator MatchQueryOperator `json:"operator,omitempty"` + } + aux := match{ + Match: f.Match, + FieldVal: f.FieldVal, + Analyzer: f.Analyzer, + BoostVal: f.BoostVal, + Prefix: f.Prefix, + Fuzziness: fuzzyValue, + Operator: f.Operator, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/match_all.go b/search/query/match_all.go new file mode 100644 index 0000000..e88825a --- /dev/null +++ b/search/query/match_all.go @@ -0,0 +1,56 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "encoding/json" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type MatchAllQuery struct { + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewMatchAllQuery creates a Query which will +// match all documents in the index. +func NewMatchAllQuery() *MatchAllQuery { + return &MatchAllQuery{} +} + +func (q *MatchAllQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *MatchAllQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *MatchAllQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + return searcher.NewMatchAllSearcher(ctx, i, q.BoostVal.Value(), options) +} + +func (q *MatchAllQuery) MarshalJSON() ([]byte, error) { + tmp := map[string]interface{}{ + "boost": q.BoostVal, + "match_all": map[string]interface{}{}, + } + return json.Marshal(tmp) +} diff --git a/search/query/match_none.go b/search/query/match_none.go new file mode 100644 index 0000000..cb65a72 --- /dev/null +++ b/search/query/match_none.go @@ -0,0 +1,56 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "encoding/json" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type MatchNoneQuery struct { + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewMatchNoneQuery creates a Query which will not +// match any documents in the index. +func NewMatchNoneQuery() *MatchNoneQuery { + return &MatchNoneQuery{} +} + +func (q *MatchNoneQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *MatchNoneQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *MatchNoneQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + return searcher.NewMatchNoneSearcher(i) +} + +func (q *MatchNoneQuery) MarshalJSON() ([]byte, error) { + tmp := map[string]interface{}{ + "boost": q.BoostVal, + "match_none": map[string]interface{}{}, + } + return json.Marshal(tmp) +} diff --git a/search/query/match_phrase.go b/search/query/match_phrase.go new file mode 100644 index 0000000..12a8396 --- /dev/null +++ b/search/query/match_phrase.go @@ -0,0 +1,176 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type MatchPhraseQuery struct { + MatchPhrase string `json:"match_phrase"` + FieldVal string `json:"field,omitempty"` + Analyzer string `json:"analyzer,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness int `json:"fuzziness"` + autoFuzzy bool +} + +// NewMatchPhraseQuery creates a new Query object +// for matching phrases in the index. +// An Analyzer is chosen based on the field. +// Input text is analyzed using this analyzer. +// Token terms resulting from this analysis are +// used to build a search phrase. Result documents +// must match this phrase. Queried field must have been indexed with +// IncludeTermVectors set to true. +func NewMatchPhraseQuery(matchPhrase string) *MatchPhraseQuery { + return &MatchPhraseQuery{ + MatchPhrase: matchPhrase, + } +} + +func (q *MatchPhraseQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *MatchPhraseQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *MatchPhraseQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *MatchPhraseQuery) SetFuzziness(f int) { + q.Fuzziness = f +} + +func (q *MatchPhraseQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + +func (q *MatchPhraseQuery) Field() string { + return q.FieldVal +} + +func (q *MatchPhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + analyzerName := "" + if q.Analyzer != "" { + analyzerName = q.Analyzer + } else { + analyzerName = m.AnalyzerNameForPath(field) + } + analyzer := m.AnalyzerNamed(analyzerName) + if analyzer == nil { + return nil, fmt.Errorf("no analyzer named '%s' registered", q.Analyzer) + } + + tokens := analyzer.Analyze([]byte(q.MatchPhrase)) + if len(tokens) > 0 { + phrase := tokenStreamToPhrase(tokens) + phraseQuery := NewMultiPhraseQuery(phrase, field) + phraseQuery.SetBoost(q.BoostVal.Value()) + if q.autoFuzzy { + phraseQuery.SetAutoFuzziness(true) + } else { + phraseQuery.SetFuzziness(q.Fuzziness) + } + return phraseQuery.Searcher(ctx, i, m, options) + } + noneQuery := NewMatchNoneQuery() + return noneQuery.Searcher(ctx, i, m, options) +} + +func tokenStreamToPhrase(tokens analysis.TokenStream) [][]string { + firstPosition := int(^uint(0) >> 1) + lastPosition := 0 + for _, token := range tokens { + if token.Position < firstPosition { + firstPosition = token.Position + } + if token.Position > lastPosition { + lastPosition = token.Position + } + } + phraseLen := lastPosition - firstPosition + 1 + if phraseLen > 0 { + rv := make([][]string, phraseLen) + for _, token := range tokens { + pos := token.Position - firstPosition + rv[pos] = append(rv[pos], string(token.Term)) + } + return rv + } + return nil +} + +func (q *MatchPhraseQuery) UnmarshalJSON(data []byte) error { + type Alias MatchPhraseQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +func (f *MatchPhraseQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type matchPhrase struct { + MatchPhrase string `json:"match_phrase"` + FieldVal string `json:"field,omitempty"` + Analyzer string `json:"analyzer,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness interface{} `json:"fuzziness"` + } + aux := matchPhrase{ + MatchPhrase: f.MatchPhrase, + FieldVal: f.FieldVal, + Analyzer: f.Analyzer, + BoostVal: f.BoostVal, + Fuzziness: fuzzyValue, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/match_phrase_test.go b/search/query/match_phrase_test.go new file mode 100644 index 0000000..8ed4888 --- /dev/null +++ b/search/query/match_phrase_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/analysis" +) + +func TestTokenStreamToPhrase(t *testing.T) { + + tests := []struct { + tokens analysis.TokenStream + result [][]string + }{ + // empty token stream returns nil + { + tokens: analysis.TokenStream{}, + result: nil, + }, + // typical token + { + tokens: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("one"), + Position: 1, + }, + &analysis.Token{ + Term: []byte("two"), + Position: 2, + }, + }, + result: [][]string{{"one"}, {"two"}}, + }, + // token stream containing a gap (usually from stop words) + { + tokens: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("wag"), + Position: 1, + }, + &analysis.Token{ + Term: []byte("dog"), + Position: 3, + }, + }, + result: [][]string{{"wag"}, nil, {"dog"}}, + }, + // token stream containing multiple tokens at the same position + { + tokens: analysis.TokenStream{ + &analysis.Token{ + Term: []byte("nia"), + Position: 1, + }, + &analysis.Token{ + Term: []byte("onia"), + Position: 1, + }, + &analysis.Token{ + Term: []byte("donia"), + Position: 1, + }, + &analysis.Token{ + Term: []byte("imo"), + Position: 2, + }, + &analysis.Token{ + Term: []byte("nimo"), + Position: 2, + }, + &analysis.Token{ + Term: []byte("ónimo"), + Position: 2, + }, + }, + result: [][]string{{"nia", "onia", "donia"}, {"imo", "nimo", "ónimo"}}, + }, + } + + for i, test := range tests { + actual := tokenStreamToPhrase(test.tokens) + if !reflect.DeepEqual(actual, test.result) { + t.Fatalf("expected %#v got %#v for test %d", test.result, actual, i) + } + } +} diff --git a/search/query/multi_phrase.go b/search/query/multi_phrase.go new file mode 100644 index 0000000..aa2cc04 --- /dev/null +++ b/search/query/multi_phrase.go @@ -0,0 +1,130 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type MultiPhraseQuery struct { + Terms [][]string `json:"terms"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness int `json:"fuzziness"` + autoFuzzy bool +} + +// NewMultiPhraseQuery creates a new Query for finding +// term phrases in the index. +// It is like PhraseQuery, but each position in the +// phrase may be satisfied by a list of terms +// as opposed to just one. +// At least one of the terms must exist in the correct +// order, at the correct index offsets, in the +// specified field. Queried field must have been indexed with +// IncludeTermVectors set to true. +func NewMultiPhraseQuery(terms [][]string, field string) *MultiPhraseQuery { + return &MultiPhraseQuery{ + Terms: terms, + FieldVal: field, + } +} + +func (q *MultiPhraseQuery) SetFuzziness(f int) { + q.Fuzziness = f +} + +func (q *MultiPhraseQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + +func (q *MultiPhraseQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *MultiPhraseQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *MultiPhraseQuery) Field() string { + return q.FieldVal +} + +func (q *MultiPhraseQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *MultiPhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + return searcher.NewMultiPhraseSearcher(ctx, i, q.Terms, q.Fuzziness, q.autoFuzzy, q.FieldVal, q.BoostVal.Value(), options) +} + +func (q *MultiPhraseQuery) Validate() error { + if len(q.Terms) < 1 { + return fmt.Errorf("phrase query must contain at least one term") + } + return nil +} + +func (q *MultiPhraseQuery) UnmarshalJSON(data []byte) error { + type Alias MultiPhraseQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +func (f *MultiPhraseQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type multiPhraseQuery struct { + Terms [][]string `json:"terms"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness interface{} `json:"fuzziness"` + } + aux := multiPhraseQuery{ + Terms: f.Terms, + FieldVal: f.FieldVal, + BoostVal: f.BoostVal, + Fuzziness: fuzzyValue, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/numeric_range.go b/search/query/numeric_range.go new file mode 100644 index 0000000..205ceec --- /dev/null +++ b/search/query/numeric_range.go @@ -0,0 +1,89 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type NumericRangeQuery struct { + Min *float64 `json:"min,omitempty"` + Max *float64 `json:"max,omitempty"` + InclusiveMin *bool `json:"inclusive_min,omitempty"` + InclusiveMax *bool `json:"inclusive_max,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewNumericRangeQuery creates a new Query for ranges +// of numeric values. +// Either, but not both endpoints can be nil. +// The minimum value is inclusive. +// The maximum value is exclusive. +func NewNumericRangeQuery(min, max *float64) *NumericRangeQuery { + return NewNumericRangeInclusiveQuery(min, max, nil, nil) +} + +// NewNumericRangeInclusiveQuery creates a new Query for ranges +// of numeric values. +// Either, but not both endpoints can be nil. +// Control endpoint inclusion with inclusiveMin, inclusiveMax. +func NewNumericRangeInclusiveQuery(min, max *float64, minInclusive, maxInclusive *bool) *NumericRangeQuery { + return &NumericRangeQuery{ + Min: min, + Max: max, + InclusiveMin: minInclusive, + InclusiveMax: maxInclusive, + } +} + +func (q *NumericRangeQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *NumericRangeQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *NumericRangeQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *NumericRangeQuery) Field() string { + return q.FieldVal +} + +func (q *NumericRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + ctx = context.WithValue(ctx, search.QueryTypeKey, search.Numeric) + return searcher.NewNumericRangeSearcher(ctx, i, q.Min, q.Max, q.InclusiveMin, q.InclusiveMax, field, q.BoostVal.Value(), options) +} + +func (q *NumericRangeQuery) Validate() error { + if q.Min == nil && q.Min == q.Max { + return fmt.Errorf("numeric range query must specify min or max") + } + return nil +} diff --git a/search/query/phrase.go b/search/query/phrase.go new file mode 100644 index 0000000..96bc1b7 --- /dev/null +++ b/search/query/phrase.go @@ -0,0 +1,127 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +type PhraseQuery struct { + Terms []string `json:"terms"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness int `json:"fuzziness"` + autoFuzzy bool +} + +// NewPhraseQuery creates a new Query for finding +// exact term phrases in the index. +// The provided terms must exist in the correct +// order, at the correct index offsets, in the +// specified field. Queried field must have been indexed with +// IncludeTermVectors set to true. +func NewPhraseQuery(terms []string, field string) *PhraseQuery { + return &PhraseQuery{ + Terms: terms, + FieldVal: field, + } +} + +func (q *PhraseQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *PhraseQuery) SetFuzziness(f int) { + q.Fuzziness = f +} + +func (q *PhraseQuery) SetAutoFuzziness(auto bool) { + q.autoFuzzy = auto +} + +func (q *PhraseQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *PhraseQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *PhraseQuery) Field() string { + return q.FieldVal +} + +func (q *PhraseQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + return searcher.NewPhraseSearcher(ctx, i, q.Terms, q.Fuzziness, q.autoFuzzy, q.FieldVal, q.BoostVal.Value(), options) +} + +func (q *PhraseQuery) Validate() error { + if len(q.Terms) < 1 { + return fmt.Errorf("phrase query must contain at least one term") + } + return nil +} + +func (q *PhraseQuery) UnmarshalJSON(data []byte) error { + type Alias PhraseQuery + aux := &struct { + Fuzziness interface{} `json:"fuzziness"` + *Alias + }{ + Alias: (*Alias)(q), + } + if err := util.UnmarshalJSON(data, &aux); err != nil { + return err + } + switch v := aux.Fuzziness.(type) { + case float64: + q.Fuzziness = int(v) + case string: + if v == "auto" { + q.autoFuzzy = true + } + } + return nil +} + +func (f *PhraseQuery) MarshalJSON() ([]byte, error) { + var fuzzyValue interface{} + if f.autoFuzzy { + fuzzyValue = "auto" + } else { + fuzzyValue = f.Fuzziness + } + type phraseQuery struct { + Terms []string `json:"terms"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` + Fuzziness interface{} `json:"fuzziness"` + } + aux := phraseQuery{ + Terms: f.Terms, + FieldVal: f.FieldVal, + BoostVal: f.BoostVal, + Fuzziness: fuzzyValue, + } + return util.MarshalJSON(aux) +} diff --git a/search/query/prefix.go b/search/query/prefix.go new file mode 100644 index 0000000..debbbc1 --- /dev/null +++ b/search/query/prefix.go @@ -0,0 +1,64 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type PrefixQuery struct { + Prefix string `json:"prefix"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewPrefixQuery creates a new Query which finds +// documents containing terms that start with the +// specified prefix. +func NewPrefixQuery(prefix string) *PrefixQuery { + return &PrefixQuery{ + Prefix: prefix, + } +} + +func (q *PrefixQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *PrefixQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *PrefixQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *PrefixQuery) Field() string { + return q.FieldVal +} + +func (q *PrefixQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + return searcher.NewTermPrefixSearcher(ctx, i, q.Prefix, field, q.BoostVal.Value(), options) +} diff --git a/search/query/query.go b/search/query/query.go new file mode 100644 index 0000000..6df38da --- /dev/null +++ b/search/query/query.go @@ -0,0 +1,783 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "strings" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + "github.com/blevesearch/bleve/v2/util" + index "github.com/blevesearch/bleve_index_api" +) + +var logger = log.New(io.Discard, "bleve mapping ", log.LstdFlags) + +// SetLog sets the logger used for logging +// by default log messages are sent to io.Discard +func SetLog(l *log.Logger) { + logger = l +} + +// A Query represents a description of the type +// and parameters for a query into the index. +type Query interface { + Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, + options search.SearcherOptions) (search.Searcher, error) +} + +// A BoostableQuery represents a Query which can be boosted +// relative to other queries. +type BoostableQuery interface { + Query + SetBoost(b float64) + Boost() float64 +} + +// A FieldableQuery represents a Query which can be restricted +// to a single field. +type FieldableQuery interface { + Query + SetField(f string) + Field() string +} + +// A ValidatableQuery represents a Query which can be validated +// prior to execution. +type ValidatableQuery interface { + Query + Validate() error +} + +// ParsePreSearchData deserializes a JSON representation of +// a PreSearchData object. +func ParsePreSearchData(input []byte) (map[string]interface{}, error) { + var rv map[string]interface{} + + var tmp map[string]json.RawMessage + err := util.UnmarshalJSON(input, &tmp) + if err != nil { + return nil, err + } + + for k, v := range tmp { + switch k { + case search.KnnPreSearchDataKey: + var value []*search.DocumentMatch + if v != nil { + err := util.UnmarshalJSON(v, &value) + if err != nil { + return nil, err + } + } + if rv == nil { + rv = make(map[string]interface{}) + } + rv[search.KnnPreSearchDataKey] = value + case search.SynonymPreSearchDataKey: + var value search.FieldTermSynonymMap + if v != nil { + err := util.UnmarshalJSON(v, &value) + if err != nil { + return nil, err + } + } + if rv == nil { + rv = make(map[string]interface{}) + } + rv[search.SynonymPreSearchDataKey] = value + case search.BM25PreSearchDataKey: + var value *search.BM25Stats + if v != nil { + err := util.UnmarshalJSON(v, &value) + if err != nil { + return nil, err + } + } + if rv == nil { + rv = make(map[string]interface{}) + } + rv[search.BM25PreSearchDataKey] = value + + } + } + return rv, nil +} + +// ParseQuery deserializes a JSON representation of +// a Query object. +func ParseQuery(input []byte) (Query, error) { + if len(input) == 0 { + // interpret as a match_none query + return NewMatchNoneQuery(), nil + } + + var tmp map[string]interface{} + err := util.UnmarshalJSON(input, &tmp) + if err != nil { + return nil, err + } + + if len(tmp) == 0 { + // interpret as a match_none query + return NewMatchNoneQuery(), nil + } + + _, hasFuzziness := tmp["fuzziness"] + _, isMatchQuery := tmp["match"] + _, isMatchPhraseQuery := tmp["match_phrase"] + _, hasTerms := tmp["terms"] + if hasFuzziness && !isMatchQuery && !isMatchPhraseQuery && !hasTerms { + var rv FuzzyQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + if isMatchQuery { + var rv MatchQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + if isMatchPhraseQuery { + var rv MatchPhraseQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + if hasTerms { + var rv PhraseQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + // now try multi-phrase + var rv2 MultiPhraseQuery + err = util.UnmarshalJSON(input, &rv2) + if err != nil { + return nil, err + } + return &rv2, nil + } + return &rv, nil + } + _, isTermQuery := tmp["term"] + if isTermQuery { + var rv TermQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasMust := tmp["must"] + _, hasShould := tmp["should"] + _, hasMustNot := tmp["must_not"] + if hasMust || hasShould || hasMustNot { + var rv BooleanQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasConjuncts := tmp["conjuncts"] + if hasConjuncts { + var rv ConjunctionQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasDisjuncts := tmp["disjuncts"] + if hasDisjuncts { + var rv DisjunctionQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + + _, hasSyntaxQuery := tmp["query"] + if hasSyntaxQuery { + var rv QueryStringQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasMin := tmp["min"].(float64) + _, hasMax := tmp["max"].(float64) + if hasMin || hasMax { + var rv NumericRangeQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasMinStr := tmp["min"].(string) + _, hasMaxStr := tmp["max"].(string) + if hasMinStr || hasMaxStr { + var rv TermRangeQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasStart := tmp["start"] + _, hasEnd := tmp["end"] + if hasStart || hasEnd { + var rv DateRangeStringQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasPrefix := tmp["prefix"] + if hasPrefix { + var rv PrefixQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasRegexp := tmp["regexp"] + if hasRegexp { + var rv RegexpQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasWildcard := tmp["wildcard"] + if hasWildcard { + var rv WildcardQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasMatchAll := tmp["match_all"] + if hasMatchAll { + var rv MatchAllQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasMatchNone := tmp["match_none"] + if hasMatchNone { + var rv MatchNoneQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasDocIds := tmp["ids"] + if hasDocIds { + var rv DocIDQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasBool := tmp["bool"] + if hasBool { + var rv BoolFieldQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasTopLeft := tmp["top_left"] + _, hasBottomRight := tmp["bottom_right"] + if hasTopLeft && hasBottomRight { + var rv GeoBoundingBoxQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasDistance := tmp["distance"] + if hasDistance { + var rv GeoDistanceQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + _, hasPoints := tmp["polygon_points"] + if hasPoints { + var rv GeoBoundingPolygonQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + + _, hasGeo := tmp["geometry"] + if hasGeo { + var rv GeoShapeQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + + _, hasCIDR := tmp["cidr"] + if hasCIDR { + var rv IPRangeQuery + err := util.UnmarshalJSON(input, &rv) + if err != nil { + return nil, err + } + return &rv, nil + } + + return nil, fmt.Errorf("unknown query type") +} + +// expandQuery traverses the input query tree and returns a new tree where +// query string queries have been expanded into base queries. Returned tree may +// reference queries from the input tree or new queries. +func expandQuery(m mapping.IndexMapping, query Query) (Query, error) { + var expand func(query Query) (Query, error) + var expandSlice func(queries []Query) ([]Query, error) = func(queries []Query) ([]Query, error) { + expanded := []Query{} + for _, q := range queries { + exp, err := expand(q) + if err != nil { + return nil, err + } + expanded = append(expanded, exp) + } + return expanded, nil + } + + expand = func(query Query) (Query, error) { + switch q := query.(type) { + case *QueryStringQuery: + parsed, err := parseQuerySyntax(q.Query) + if err != nil { + return nil, fmt.Errorf("could not parse '%s': %s", q.Query, err) + } + return expand(parsed) + case *ConjunctionQuery: + children, err := expandSlice(q.Conjuncts) + if err != nil { + return nil, err + } + q.Conjuncts = children + return q, nil + case *DisjunctionQuery: + children, err := expandSlice(q.Disjuncts) + if err != nil { + return nil, err + } + q.Disjuncts = children + return q, nil + case *BooleanQuery: + var err error + q.Must, err = expand(q.Must) + if err != nil { + return nil, err + } + q.Should, err = expand(q.Should) + if err != nil { + return nil, err + } + q.MustNot, err = expand(q.MustNot) + if err != nil { + return nil, err + } + return q, nil + default: + return query, nil + } + } + return expand(query) +} + +// DumpQuery returns a string representation of the query tree, where query +// string queries have been expanded into base queries. The output format is +// meant for debugging purpose and may change in the future. +func DumpQuery(m mapping.IndexMapping, query Query) (string, error) { + q, err := expandQuery(m, query) + if err != nil { + return "", err + } + data, err := json.MarshalIndent(q, "", " ") + return string(data), err +} + +// FieldSet represents a set of queried fields. +type FieldSet map[string]struct{} + +// ExtractFields returns a set of fields referenced by the query. +// The returned set may be nil if the query does not explicitly reference any field +// and the DefaultSearchField is unset in the index mapping. +func ExtractFields(q Query, m mapping.IndexMapping, fs FieldSet) (FieldSet, error) { + if q == nil || m == nil { + return fs, nil + } + var err error + switch q := q.(type) { + case FieldableQuery: + f := q.Field() + if f == "" { + f = m.DefaultSearchField() + } + if f != "" { + if fs == nil { + fs = make(FieldSet) + } + fs[f] = struct{}{} + } + case *QueryStringQuery: + var expandedQuery Query + expandedQuery, err = expandQuery(m, q) + if err == nil { + fs, err = ExtractFields(expandedQuery, m, fs) + } + case *BooleanQuery: + for _, subq := range []Query{q.Must, q.Should, q.MustNot} { + fs, err = ExtractFields(subq, m, fs) + if err != nil { + break + } + } + case *ConjunctionQuery: + for _, subq := range q.Conjuncts { + fs, err = ExtractFields(subq, m, fs) + if err != nil { + break + } + } + case *DisjunctionQuery: + for _, subq := range q.Disjuncts { + fs, err = ExtractFields(subq, m, fs) + if err != nil { + break + } + } + } + return fs, err +} + +const ( + FuzzyMatchType = iota + RegexpMatchType + PrefixMatchType +) + +// ExtractSynonyms extracts synonyms from the query tree and returns a map of +// field-term pairs to their synonyms. The input query tree is traversed and +// for each term query, the synonyms are extracted from the synonym source +// associated with the field. The synonyms are then added to the provided map. +// The map is returned and may be nil if no synonyms were found. +func ExtractSynonyms(ctx context.Context, m mapping.SynonymMapping, r index.ThesaurusReader, + query Query, rv search.FieldTermSynonymMap, +) (search.FieldTermSynonymMap, error) { + if r == nil || m == nil || query == nil { + return rv, nil + } + var err error + resolveFieldAndSource := func(field string) (string, string) { + if field == "" { + field = m.DefaultSearchField() + } + return field, m.SynonymSourceForPath(field) + } + handleAnalyzer := func(analyzerName, field string) (analysis.Analyzer, error) { + if analyzerName == "" { + analyzerName = m.AnalyzerNameForPath(field) + } + analyzer := m.AnalyzerNamed(analyzerName) + if analyzer == nil { + return nil, fmt.Errorf("no analyzer named '%s' registered", analyzerName) + } + return analyzer, nil + } + switch q := query.(type) { + case *BooleanQuery: + rv, err = ExtractSynonyms(ctx, m, r, q.Must, rv) + if err != nil { + return nil, err + } + rv, err = ExtractSynonyms(ctx, m, r, q.Should, rv) + if err != nil { + return nil, err + } + rv, err = ExtractSynonyms(ctx, m, r, q.MustNot, rv) + if err != nil { + return nil, err + } + case *ConjunctionQuery: + for _, child := range q.Conjuncts { + rv, err = ExtractSynonyms(ctx, m, r, child, rv) + if err != nil { + return nil, err + } + } + case *DisjunctionQuery: + for _, child := range q.Disjuncts { + rv, err = ExtractSynonyms(ctx, m, r, child, rv) + if err != nil { + return nil, err + } + } + case *FuzzyQuery: + field, source := resolveFieldAndSource(q.FieldVal) + if source != "" { + fuzziness := q.Fuzziness + if q.autoFuzzy { + fuzziness = searcher.GetAutoFuzziness(q.Term) + } + rv, err = addSynonymsForTermWithMatchType(ctx, FuzzyMatchType, source, field, q.Term, fuzziness, q.Prefix, r, rv) + if err != nil { + return nil, err + } + } + case *MatchQuery, *MatchPhraseQuery: + var analyzerName, matchString, fieldVal string + var fuzziness, prefix int + var autoFuzzy bool + if mq, ok := q.(*MatchQuery); ok { + analyzerName, fieldVal, matchString, fuzziness, prefix, autoFuzzy = mq.Analyzer, mq.FieldVal, mq.Match, mq.Fuzziness, mq.Prefix, mq.autoFuzzy + } else if mpq, ok := q.(*MatchPhraseQuery); ok { + analyzerName, fieldVal, matchString, fuzziness, autoFuzzy = mpq.Analyzer, mpq.FieldVal, mpq.MatchPhrase, mpq.Fuzziness, mpq.autoFuzzy + } + field, source := resolveFieldAndSource(fieldVal) + if source != "" { + analyzer, err := handleAnalyzer(analyzerName, field) + if err != nil { + return nil, err + } + tokens := analyzer.Analyze([]byte(matchString)) + for _, token := range tokens { + if autoFuzzy { + fuzziness = searcher.GetAutoFuzziness(string(token.Term)) + } + rv, err = addSynonymsForTermWithMatchType(ctx, FuzzyMatchType, source, field, string(token.Term), fuzziness, prefix, r, rv) + if err != nil { + return nil, err + } + } + } + case *MultiPhraseQuery, *PhraseQuery: + var fieldVal string + var fuzziness int + var autoFuzzy bool + if mpq, ok := q.(*MultiPhraseQuery); ok { + fieldVal, fuzziness, autoFuzzy = mpq.FieldVal, mpq.Fuzziness, mpq.autoFuzzy + } else if pq, ok := q.(*PhraseQuery); ok { + fieldVal, fuzziness, autoFuzzy = pq.FieldVal, pq.Fuzziness, pq.autoFuzzy + } + field, source := resolveFieldAndSource(fieldVal) + if source != "" { + var terms []string + if mpq, ok := q.(*MultiPhraseQuery); ok { + for _, termGroup := range mpq.Terms { + terms = append(terms, termGroup...) + } + } else if pq, ok := q.(*PhraseQuery); ok { + terms = pq.Terms + } + for _, term := range terms { + if autoFuzzy { + fuzziness = searcher.GetAutoFuzziness(term) + } + rv, err = addSynonymsForTermWithMatchType(ctx, FuzzyMatchType, source, field, term, fuzziness, 0, r, rv) + if err != nil { + return nil, err + } + } + } + case *PrefixQuery: + field, source := resolveFieldAndSource(q.FieldVal) + if source != "" { + rv, err = addSynonymsForTermWithMatchType(ctx, PrefixMatchType, source, field, q.Prefix, 0, 0, r, rv) + if err != nil { + return nil, err + } + } + case *QueryStringQuery: + expanded, err := expandQuery(m, q) + if err != nil { + return nil, err + } + rv, err = ExtractSynonyms(ctx, m, r, expanded, rv) + if err != nil { + return nil, err + } + case *TermQuery: + field, source := resolveFieldAndSource(q.FieldVal) + if source != "" { + rv, err = addSynonymsForTerm(ctx, source, field, q.Term, r, rv) + if err != nil { + return nil, err + } + } + case *RegexpQuery: + field, source := resolveFieldAndSource(q.FieldVal) + if source != "" { + rv, err = addSynonymsForTermWithMatchType(ctx, RegexpMatchType, source, field, strings.TrimPrefix(q.Regexp, "^"), 0, 0, r, rv) + if err != nil { + return nil, err + } + } + case *WildcardQuery: + field, source := resolveFieldAndSource(q.FieldVal) + if source != "" { + rv, err = addSynonymsForTermWithMatchType(ctx, RegexpMatchType, source, field, wildcardRegexpReplacer.Replace(q.Wildcard), 0, 0, r, rv) + if err != nil { + return nil, err + } + } + } + return rv, nil +} + +// addFuzzySynonymsForTerm finds all terms that match the given term with the +// given fuzziness and adds their synonyms to the provided map. +func addSynonymsForTermWithMatchType(ctx context.Context, matchType int, src, field, term string, fuzziness, prefix int, + r index.ThesaurusReader, rv search.FieldTermSynonymMap, +) (search.FieldTermSynonymMap, error) { + // Determine the terms based on the match type (fuzzy, prefix, or regexp) + var thesKeys index.ThesaurusKeys + var err error + var terms []string + switch matchType { + case FuzzyMatchType: + // Ensure valid fuzziness + if fuzziness == 0 { + rv, err = addSynonymsForTerm(ctx, src, field, term, r, rv) + if err != nil { + return nil, err + } + return rv, nil + } + if fuzziness > searcher.MaxFuzziness { + return nil, fmt.Errorf("fuzziness exceeds max (%d)", searcher.MaxFuzziness) + } + if fuzziness < 0 { + return nil, fmt.Errorf("invalid fuzziness, negative") + } + // Handle fuzzy match + prefixTerm := "" + for i, r := range term { + if i < prefix { + prefixTerm += string(r) + } else { + break + } + } + thesKeys, err = r.ThesaurusKeysFuzzy(src, term, fuzziness, prefixTerm) + case RegexpMatchType: + // Handle regexp match + thesKeys, err = r.ThesaurusKeysRegexp(src, term) + case PrefixMatchType: + // Handle prefix match + thesKeys, err = r.ThesaurusKeysPrefix(src, []byte(term)) + default: + return nil, fmt.Errorf("invalid match type: %d", matchType) + } + if err != nil { + return nil, err + } + defer func() { + if cerr := thesKeys.Close(); cerr != nil && err == nil { + err = cerr + } + }() + // Collect the matching terms + terms = []string{} + tfd, err := thesKeys.Next() + for err == nil && tfd != nil { + terms = append(terms, tfd.Term) + tfd, err = thesKeys.Next() + } + if err != nil { + return nil, err + } + for _, synTerm := range terms { + rv, err = addSynonymsForTerm(ctx, src, field, synTerm, r, rv) + if err != nil { + return nil, err + } + } + return rv, nil +} + +func addSynonymsForTerm(ctx context.Context, src, field, term string, + r index.ThesaurusReader, rv search.FieldTermSynonymMap, +) (search.FieldTermSynonymMap, error) { + termReader, err := r.ThesaurusTermReader(ctx, src, []byte(term)) + if err != nil { + return nil, err + } + defer func() { + if cerr := termReader.Close(); cerr != nil && err == nil { + err = cerr + } + }() + var synonyms []string + synonym, err := termReader.Next() + for err == nil && synonym != "" { + synonyms = append(synonyms, synonym) + synonym, err = termReader.Next() + } + if err != nil { + return nil, err + } + if len(synonyms) > 0 { + if rv == nil { + rv = make(search.FieldTermSynonymMap) + } + if _, exists := rv[field]; !exists { + rv[field] = make(map[string][]string) + } + rv[field][term] = synonyms + } + return rv, nil +} diff --git a/search/query/query_string.go b/search/query/query_string.go new file mode 100644 index 0000000..42bb598 --- /dev/null +++ b/search/query/query_string.go @@ -0,0 +1,69 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +type QueryStringQuery struct { + Query string `json:"query"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewQueryStringQuery creates a new Query used for +// finding documents that satisfy a query string. The +// query string is a small query language for humans. +func NewQueryStringQuery(query string) *QueryStringQuery { + return &QueryStringQuery{ + Query: query, + } +} + +func (q *QueryStringQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *QueryStringQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *QueryStringQuery) Parse() (Query, error) { + return parseQuerySyntax(q.Query) +} + +func (q *QueryStringQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + newQuery, err := parseQuerySyntax(q.Query) + if err != nil { + return nil, err + } + return newQuery.Searcher(ctx, i, m, options) +} + +func (q *QueryStringQuery) Validate() error { + newQuery, err := parseQuerySyntax(q.Query) + if err != nil { + return err + } + if newQuery, ok := newQuery.(ValidatableQuery); ok { + return newQuery.Validate() + } + return nil +} diff --git a/search/query/query_string.y b/search/query/query_string.y new file mode 100644 index 0000000..aeec856 --- /dev/null +++ b/search/query/query_string.y @@ -0,0 +1,338 @@ +%{ +package query +import ( + "fmt" + "strconv" + "strings" + "time" +) + +func logDebugGrammar(format string, v ...interface{}) { + if debugParser { + logger.Printf(format, v...) + } +} +%} + +%union { +s string +n int +f float64 +q Query +pf *float64} + +%token tSTRING tPHRASE tPLUS tMINUS tCOLON tBOOST tNUMBER tSTRING tGREATER tLESS +tEQUAL tTILDE + +%type tSTRING +%type tPHRASE +%type tNUMBER +%type posOrNegNumber +%type fieldName +%type tTILDE +%type tBOOST +%type searchBase +%type searchSuffix +%type searchPrefix + +%% + +input: +searchParts { + logDebugGrammar("INPUT") +}; + +searchParts: +searchPart searchParts { + logDebugGrammar("SEARCH PARTS") +} +| +searchPart { + logDebugGrammar("SEARCH PART") +}; + +searchPart: +searchPrefix searchBase searchSuffix { + query := $2 + if $3 != nil { + if query, ok := query.(BoostableQuery); ok { + query.SetBoost(*$3) + } + } + switch($1) { + case queryShould: + yylex.(*lexerWrapper).query.AddShould(query) + case queryMust: + yylex.(*lexerWrapper).query.AddMust(query) + case queryMustNot: + yylex.(*lexerWrapper).query.AddMustNot(query) + } +}; + + +searchPrefix: +/* empty */ { + $$ = queryShould +} +| +tPLUS { + logDebugGrammar("PLUS") + $$ = queryMust +} +| +tMINUS { + logDebugGrammar("MINUS") + $$ = queryMustNot +}; + +searchBase: +tSTRING { + str := $1 + logDebugGrammar("STRING - %s", str) + var q FieldableQuery + if strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { + q = NewRegexpQuery(str[1:len(str)-1]) + } else if strings.ContainsAny(str, "*?"){ + q = NewWildcardQuery(str) + } else { + q = NewMatchQuery(str) + } + $$ = q +} +| +tSTRING tTILDE { + str := $1 + fuzziness, err := strconv.ParseFloat($2, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid fuzziness value: %v", err)) + } + logDebugGrammar("FUZZY STRING - %s %f", str, fuzziness) + q := NewMatchQuery(str) + q.SetFuzziness(int(fuzziness)) + $$ = q +} +| +fieldName tCOLON tSTRING tTILDE { + field := $1 + str := $3 + fuzziness, err := strconv.ParseFloat($4, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid fuzziness value: %v", err)) + } + logDebugGrammar("FIELD - %s FUZZY STRING - %s %f", field, str, fuzziness) + q := NewMatchQuery(str) + q.SetFuzziness(int(fuzziness)) + q.SetField(field) + $$ = q +} +| +tNUMBER { + str := $1 + logDebugGrammar("STRING - %s", str) + q1 := NewMatchQuery(str) + val, err := strconv.ParseFloat($1, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + inclusive := true + q2 := NewNumericRangeInclusiveQuery(&val, &val, &inclusive, &inclusive) + q := NewDisjunctionQuery([]Query{q1,q2}) + q.queryStringMode = true + $$ = q +} +| +tPHRASE { + phrase := $1 + logDebugGrammar("PHRASE - %s", phrase) + q := NewMatchPhraseQuery(phrase) + $$ = q +} +| +fieldName tCOLON tSTRING { + field := $1 + str := $3 + logDebugGrammar("FIELD - %s STRING - %s", field, str) + var q FieldableQuery + if strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { + q = NewRegexpQuery(str[1:len(str)-1]) + } else if strings.ContainsAny(str, "*?"){ + q = NewWildcardQuery(str) + } else { + q = NewMatchQuery(str) + } + q.SetField(field) + $$ = q +} +| +fieldName tCOLON posOrNegNumber { + field := $1 + str := $3 + logDebugGrammar("FIELD - %s STRING - %s", field, str) + q1 := NewMatchQuery(str) + q1.SetField(field) + val, err := strconv.ParseFloat($3, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + inclusive := true + q2 := NewNumericRangeInclusiveQuery(&val, &val, &inclusive, &inclusive) + q2.SetField(field) + q := NewDisjunctionQuery([]Query{q1,q2}) + q.queryStringMode = true + $$ = q +} +| +fieldName tCOLON tPHRASE { + field := $1 + phrase := $3 + logDebugGrammar("FIELD - %s PHRASE - %s", field, phrase) + q := NewMatchPhraseQuery(phrase) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tGREATER posOrNegNumber { + field := $1 + min, err := strconv.ParseFloat($4, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + minInclusive := false + logDebugGrammar("FIELD - GREATER THAN %f", min) + q := NewNumericRangeInclusiveQuery(&min, nil, &minInclusive, nil) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tGREATER tEQUAL posOrNegNumber { + field := $1 + min, err := strconv.ParseFloat($5, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + minInclusive := true + logDebugGrammar("FIELD - GREATER THAN OR EQUAL %f", min) + q := NewNumericRangeInclusiveQuery(&min, nil, &minInclusive, nil) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tLESS posOrNegNumber { + field := $1 + max, err := strconv.ParseFloat($4, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + maxInclusive := false + logDebugGrammar("FIELD - LESS THAN %f", max) + q := NewNumericRangeInclusiveQuery(nil, &max, nil, &maxInclusive) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tLESS tEQUAL posOrNegNumber { + field := $1 + max, err := strconv.ParseFloat($5, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + maxInclusive := true + logDebugGrammar("FIELD - LESS THAN OR EQUAL %f", max) + q := NewNumericRangeInclusiveQuery(nil, &max, nil, &maxInclusive) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tGREATER tPHRASE { + field := $1 + minInclusive := false + phrase := $4 + + logDebugGrammar("FIELD - GREATER THAN DATE %s", phrase) + minTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(minTime, time.Time{}, &minInclusive, nil) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tGREATER tEQUAL tPHRASE { + field := $1 + minInclusive := true + phrase := $5 + + logDebugGrammar("FIELD - GREATER THAN OR EQUAL DATE %s", phrase) + minTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(minTime, time.Time{}, &minInclusive, nil) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tLESS tPHRASE { + field := $1 + maxInclusive := false + phrase := $4 + + logDebugGrammar("FIELD - LESS THAN DATE %s", phrase) + maxTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(time.Time{}, maxTime, nil, &maxInclusive) + q.SetField(field) + $$ = q +} +| +fieldName tCOLON tLESS tEQUAL tPHRASE { + field := $1 + maxInclusive := true + phrase := $5 + + logDebugGrammar("FIELD - LESS THAN OR EQUAL DATE %s", phrase) + maxTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(time.Time{}, maxTime, nil, &maxInclusive) + q.SetField(field) + $$ = q +}; + +searchSuffix: +/* empty */ { + $$ = nil +} +| +tBOOST { + $$ = nil + boost, err := strconv.ParseFloat($1, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid boost value: %v", err)) + } else { + $$ = &boost + } + logDebugGrammar("BOOST %f", boost) +}; + +posOrNegNumber: +tNUMBER { + $$ = $1 +} +| +tMINUS tNUMBER { + $$ = "-" + $2 +}; + +fieldName: +tPHRASE { + $$ = $1 +} +| +tSTRING { + $$ = $1 +}; diff --git a/search/query/query_string.y.go b/search/query/query_string.y.go new file mode 100644 index 0000000..3a2abc1 --- /dev/null +++ b/search/query/query_string.y.go @@ -0,0 +1,833 @@ +// Code generated by goyacc -o query_string.y.go query_string.y. DO NOT EDIT. + +//line query_string.y:2 +package query + +import __yyfmt__ "fmt" + +//line query_string.y:2 +import ( + "fmt" + "strconv" + "strings" + "time" +) + +func logDebugGrammar(format string, v ...interface{}) { + if debugParser { + logger.Printf(format, v...) + } +} + +//line query_string.y:17 +type yySymType struct { + yys int + s string + n int + f float64 + q Query + pf *float64 +} + +const tSTRING = 57346 +const tPHRASE = 57347 +const tPLUS = 57348 +const tMINUS = 57349 +const tCOLON = 57350 +const tBOOST = 57351 +const tNUMBER = 57352 +const tGREATER = 57353 +const tLESS = 57354 +const tEQUAL = 57355 +const tTILDE = 57356 + +var yyToknames = [...]string{ + "$end", + "error", + "$unk", + "tSTRING", + "tPHRASE", + "tPLUS", + "tMINUS", + "tCOLON", + "tBOOST", + "tNUMBER", + "tGREATER", + "tLESS", + "tEQUAL", + "tTILDE", +} + +var yyStatenames = [...]string{} + +const yyEofCode = 1 +const yyErrCode = 2 +const yyInitialStackSize = 16 + +//line yacctab:1 +var yyExca = [...]int{ + -1, 1, + 1, -1, + -2, 0, + -1, 3, + 1, 3, + -2, 5, + -1, 9, + 8, 29, + -2, 8, + -1, 12, + 8, 28, + -2, 12, +} + +const yyPrivate = 57344 + +const yyLast = 43 + +var yyAct = [...]int{ + 18, 17, 19, 24, 23, 15, 31, 22, 20, 21, + 30, 27, 23, 23, 3, 22, 22, 14, 29, 26, + 16, 25, 28, 35, 33, 23, 23, 32, 22, 22, + 34, 9, 12, 1, 5, 6, 2, 11, 4, 13, + 7, 8, 10, +} + +var yyPact = [...]int{ + 28, -1000, -1000, 28, 27, -1000, -1000, -1000, 8, -9, + 12, -1000, -1000, -1000, -1000, -1000, -3, -11, -1000, -1000, + 6, 5, -1000, -4, -1000, -1000, 19, -1000, -1000, 18, + -1000, -1000, -1000, -1000, -1000, -1000, +} + +var yyPgo = [...]int{ + 0, 0, 42, 41, 39, 38, 33, 36, 14, +} + +var yyR1 = [...]int{ + 0, 6, 7, 7, 8, 5, 5, 5, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 4, 4, 1, 1, 2, 2, +} + +var yyR2 = [...]int{ + 0, 1, 2, 1, 3, 0, 1, 1, 1, 2, + 4, 1, 1, 3, 3, 3, 4, 5, 4, 5, + 4, 5, 4, 5, 0, 1, 1, 2, 1, 1, +} + +var yyChk = [...]int{ + -1000, -6, -7, -8, -5, 6, 7, -7, -3, 4, + -2, 10, 5, -4, 9, 14, 8, 4, -1, 5, + 11, 12, 10, 7, 14, -1, 13, 5, -1, 13, + 5, 10, -1, 5, -1, 5, +} + +var yyDef = [...]int{ + 5, -2, 1, -2, 0, 6, 7, 2, 24, -2, + 0, 11, -2, 4, 25, 9, 0, 13, 14, 15, + 0, 0, 26, 0, 10, 16, 0, 20, 18, 0, + 22, 27, 17, 21, 19, 23, +} + +var yyTok1 = [...]int{ + 1, +} + +var yyTok2 = [...]int{ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, +} + +var yyTok3 = [...]int{ + 0, +} + +var yyErrorMessages = [...]struct { + state int + token int + msg string +}{} + +//line yaccpar:1 + +/* parser for yacc output */ + +var ( + yyDebug = 0 + yyErrorVerbose = false +) + +type yyLexer interface { + Lex(lval *yySymType) int + Error(s string) +} + +type yyParser interface { + Parse(yyLexer) int + Lookahead() int +} + +type yyParserImpl struct { + lval yySymType + stack [yyInitialStackSize]yySymType + char int +} + +func (p *yyParserImpl) Lookahead() int { + return p.char +} + +func yyNewParser() yyParser { + return &yyParserImpl{} +} + +const yyFlag = -1000 + +func yyTokname(c int) string { + if c >= 1 && c-1 < len(yyToknames) { + if yyToknames[c-1] != "" { + return yyToknames[c-1] + } + } + return __yyfmt__.Sprintf("tok-%v", c) +} + +func yyStatname(s int) string { + if s >= 0 && s < len(yyStatenames) { + if yyStatenames[s] != "" { + return yyStatenames[s] + } + } + return __yyfmt__.Sprintf("state-%v", s) +} + +func yyErrorMessage(state, lookAhead int) string { + const TOKSTART = 4 + + if !yyErrorVerbose { + return "syntax error" + } + + for _, e := range yyErrorMessages { + if e.state == state && e.token == lookAhead { + return "syntax error: " + e.msg + } + } + + res := "syntax error: unexpected " + yyTokname(lookAhead) + + // To match Bison, suggest at most four expected tokens. + expected := make([]int, 0, 4) + + // Look for shiftable tokens. + base := yyPact[state] + for tok := TOKSTART; tok-1 < len(yyToknames); tok++ { + if n := base + tok; n >= 0 && n < yyLast && yyChk[yyAct[n]] == tok { + if len(expected) == cap(expected) { + return res + } + expected = append(expected, tok) + } + } + + if yyDef[state] == -2 { + i := 0 + for yyExca[i] != -1 || yyExca[i+1] != state { + i += 2 + } + + // Look for tokens that we accept or reduce. + for i += 2; yyExca[i] >= 0; i += 2 { + tok := yyExca[i] + if tok < TOKSTART || yyExca[i+1] == 0 { + continue + } + if len(expected) == cap(expected) { + return res + } + expected = append(expected, tok) + } + + // If the default action is to accept or reduce, give up. + if yyExca[i+1] != 0 { + return res + } + } + + for i, tok := range expected { + if i == 0 { + res += ", expecting " + } else { + res += " or " + } + res += yyTokname(tok) + } + return res +} + +func yylex1(lex yyLexer, lval *yySymType) (char, token int) { + token = 0 + char = lex.Lex(lval) + if char <= 0 { + token = yyTok1[0] + goto out + } + if char < len(yyTok1) { + token = yyTok1[char] + goto out + } + if char >= yyPrivate { + if char < yyPrivate+len(yyTok2) { + token = yyTok2[char-yyPrivate] + goto out + } + } + for i := 0; i < len(yyTok3); i += 2 { + token = yyTok3[i+0] + if token == char { + token = yyTok3[i+1] + goto out + } + } + +out: + if token == 0 { + token = yyTok2[1] /* unknown char */ + } + if yyDebug >= 3 { + __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char)) + } + return char, token +} + +func yyParse(yylex yyLexer) int { + return yyNewParser().Parse(yylex) +} + +func (yyrcvr *yyParserImpl) Parse(yylex yyLexer) int { + var yyn int + var yyVAL yySymType + var yyDollar []yySymType + _ = yyDollar // silence set and not used + yyS := yyrcvr.stack[:] + + Nerrs := 0 /* number of errors */ + Errflag := 0 /* error recovery flag */ + yystate := 0 + yyrcvr.char = -1 + yytoken := -1 // yyrcvr.char translated into internal numbering + defer func() { + // Make sure we report no lookahead when not parsing. + yystate = -1 + yyrcvr.char = -1 + yytoken = -1 + }() + yyp := -1 + goto yystack + +ret0: + return 0 + +ret1: + return 1 + +yystack: + /* put a state and value onto the stack */ + if yyDebug >= 4 { + __yyfmt__.Printf("char %v in %v\n", yyTokname(yytoken), yyStatname(yystate)) + } + + yyp++ + if yyp >= len(yyS) { + nyys := make([]yySymType, len(yyS)*2) + copy(nyys, yyS) + yyS = nyys + } + yyS[yyp] = yyVAL + yyS[yyp].yys = yystate + +yynewstate: + yyn = yyPact[yystate] + if yyn <= yyFlag { + goto yydefault /* simple state */ + } + if yyrcvr.char < 0 { + yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) + } + yyn += yytoken + if yyn < 0 || yyn >= yyLast { + goto yydefault + } + yyn = yyAct[yyn] + if yyChk[yyn] == yytoken { /* valid shift */ + yyrcvr.char = -1 + yytoken = -1 + yyVAL = yyrcvr.lval + yystate = yyn + if Errflag > 0 { + Errflag-- + } + goto yystack + } + +yydefault: + /* default state action */ + yyn = yyDef[yystate] + if yyn == -2 { + if yyrcvr.char < 0 { + yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) + } + + /* look through exception table */ + xi := 0 + for { + if yyExca[xi+0] == -1 && yyExca[xi+1] == yystate { + break + } + xi += 2 + } + for xi += 2; ; xi += 2 { + yyn = yyExca[xi+0] + if yyn < 0 || yyn == yytoken { + break + } + } + yyn = yyExca[xi+1] + if yyn < 0 { + goto ret0 + } + } + if yyn == 0 { + /* error ... attempt to resume parsing */ + switch Errflag { + case 0: /* brand new error */ + yylex.Error(yyErrorMessage(yystate, yytoken)) + Nerrs++ + if yyDebug >= 1 { + __yyfmt__.Printf("%s", yyStatname(yystate)) + __yyfmt__.Printf(" saw %s\n", yyTokname(yytoken)) + } + fallthrough + + case 1, 2: /* incompletely recovered error ... try again */ + Errflag = 3 + + /* find a state where "error" is a legal shift action */ + for yyp >= 0 { + yyn = yyPact[yyS[yyp].yys] + yyErrCode + if yyn >= 0 && yyn < yyLast { + yystate = yyAct[yyn] /* simulate a shift of "error" */ + if yyChk[yystate] == yyErrCode { + goto yystack + } + } + + /* the current p has no shift on "error", pop stack */ + if yyDebug >= 2 { + __yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys) + } + yyp-- + } + /* there is no state on the stack with an error shift ... abort */ + goto ret1 + + case 3: /* no shift yet; clobber input char */ + if yyDebug >= 2 { + __yyfmt__.Printf("error recovery discards %s\n", yyTokname(yytoken)) + } + if yytoken == yyEofCode { + goto ret1 + } + yyrcvr.char = -1 + yytoken = -1 + goto yynewstate /* try again in the same state */ + } + } + + /* reduction by production yyn */ + if yyDebug >= 2 { + __yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate)) + } + + yynt := yyn + yypt := yyp + _ = yypt // guard against "declared and not used" + + yyp -= yyR2[yyn] + // yyp is now the index of $0. Perform the default action. Iff the + // reduced production is ε, $1 is possibly out of range. + if yyp+1 >= len(yyS) { + nyys := make([]yySymType, len(yyS)*2) + copy(nyys, yyS) + yyS = nyys + } + yyVAL = yyS[yyp+1] + + /* consult goto table to find next state */ + yyn = yyR1[yyn] + yyg := yyPgo[yyn] + yyj := yyg + yyS[yyp].yys + 1 + + if yyj >= yyLast { + yystate = yyAct[yyg] + } else { + yystate = yyAct[yyj] + if yyChk[yystate] != -yyn { + yystate = yyAct[yyg] + } + } + // dummy call; replaced with literal code + switch yynt { + + case 1: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:41 + { + logDebugGrammar("INPUT") + } + case 2: + yyDollar = yyS[yypt-2 : yypt+1] +//line query_string.y:46 + { + logDebugGrammar("SEARCH PARTS") + } + case 3: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:50 + { + logDebugGrammar("SEARCH PART") + } + case 4: + yyDollar = yyS[yypt-3 : yypt+1] +//line query_string.y:55 + { + query := yyDollar[2].q + if yyDollar[3].pf != nil { + if query, ok := query.(BoostableQuery); ok { + query.SetBoost(*yyDollar[3].pf) + } + } + switch yyDollar[1].n { + case queryShould: + yylex.(*lexerWrapper).query.AddShould(query) + case queryMust: + yylex.(*lexerWrapper).query.AddMust(query) + case queryMustNot: + yylex.(*lexerWrapper).query.AddMustNot(query) + } + } + case 5: + yyDollar = yyS[yypt-0 : yypt+1] +//line query_string.y:74 + { + yyVAL.n = queryShould + } + case 6: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:78 + { + logDebugGrammar("PLUS") + yyVAL.n = queryMust + } + case 7: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:83 + { + logDebugGrammar("MINUS") + yyVAL.n = queryMustNot + } + case 8: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:89 + { + str := yyDollar[1].s + logDebugGrammar("STRING - %s", str) + var q FieldableQuery + if strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { + q = NewRegexpQuery(str[1 : len(str)-1]) + } else if strings.ContainsAny(str, "*?") { + q = NewWildcardQuery(str) + } else { + q = NewMatchQuery(str) + } + yyVAL.q = q + } + case 9: + yyDollar = yyS[yypt-2 : yypt+1] +//line query_string.y:103 + { + str := yyDollar[1].s + fuzziness, err := strconv.ParseFloat(yyDollar[2].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid fuzziness value: %v", err)) + } + logDebugGrammar("FUZZY STRING - %s %f", str, fuzziness) + q := NewMatchQuery(str) + q.SetFuzziness(int(fuzziness)) + yyVAL.q = q + } + case 10: + yyDollar = yyS[yypt-4 : yypt+1] +//line query_string.y:115 + { + field := yyDollar[1].s + str := yyDollar[3].s + fuzziness, err := strconv.ParseFloat(yyDollar[4].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid fuzziness value: %v", err)) + } + logDebugGrammar("FIELD - %s FUZZY STRING - %s %f", field, str, fuzziness) + q := NewMatchQuery(str) + q.SetFuzziness(int(fuzziness)) + q.SetField(field) + yyVAL.q = q + } + case 11: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:129 + { + str := yyDollar[1].s + logDebugGrammar("STRING - %s", str) + q1 := NewMatchQuery(str) + val, err := strconv.ParseFloat(yyDollar[1].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + inclusive := true + q2 := NewNumericRangeInclusiveQuery(&val, &val, &inclusive, &inclusive) + q := NewDisjunctionQuery([]Query{q1, q2}) + q.queryStringMode = true + yyVAL.q = q + } + case 12: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:144 + { + phrase := yyDollar[1].s + logDebugGrammar("PHRASE - %s", phrase) + q := NewMatchPhraseQuery(phrase) + yyVAL.q = q + } + case 13: + yyDollar = yyS[yypt-3 : yypt+1] +//line query_string.y:151 + { + field := yyDollar[1].s + str := yyDollar[3].s + logDebugGrammar("FIELD - %s STRING - %s", field, str) + var q FieldableQuery + if strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { + q = NewRegexpQuery(str[1 : len(str)-1]) + } else if strings.ContainsAny(str, "*?") { + q = NewWildcardQuery(str) + } else { + q = NewMatchQuery(str) + } + q.SetField(field) + yyVAL.q = q + } + case 14: + yyDollar = yyS[yypt-3 : yypt+1] +//line query_string.y:167 + { + field := yyDollar[1].s + str := yyDollar[3].s + logDebugGrammar("FIELD - %s STRING - %s", field, str) + q1 := NewMatchQuery(str) + q1.SetField(field) + val, err := strconv.ParseFloat(yyDollar[3].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + inclusive := true + q2 := NewNumericRangeInclusiveQuery(&val, &val, &inclusive, &inclusive) + q2.SetField(field) + q := NewDisjunctionQuery([]Query{q1, q2}) + q.queryStringMode = true + yyVAL.q = q + } + case 15: + yyDollar = yyS[yypt-3 : yypt+1] +//line query_string.y:185 + { + field := yyDollar[1].s + phrase := yyDollar[3].s + logDebugGrammar("FIELD - %s PHRASE - %s", field, phrase) + q := NewMatchPhraseQuery(phrase) + q.SetField(field) + yyVAL.q = q + } + case 16: + yyDollar = yyS[yypt-4 : yypt+1] +//line query_string.y:194 + { + field := yyDollar[1].s + min, err := strconv.ParseFloat(yyDollar[4].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + minInclusive := false + logDebugGrammar("FIELD - GREATER THAN %f", min) + q := NewNumericRangeInclusiveQuery(&min, nil, &minInclusive, nil) + q.SetField(field) + yyVAL.q = q + } + case 17: + yyDollar = yyS[yypt-5 : yypt+1] +//line query_string.y:207 + { + field := yyDollar[1].s + min, err := strconv.ParseFloat(yyDollar[5].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + minInclusive := true + logDebugGrammar("FIELD - GREATER THAN OR EQUAL %f", min) + q := NewNumericRangeInclusiveQuery(&min, nil, &minInclusive, nil) + q.SetField(field) + yyVAL.q = q + } + case 18: + yyDollar = yyS[yypt-4 : yypt+1] +//line query_string.y:220 + { + field := yyDollar[1].s + max, err := strconv.ParseFloat(yyDollar[4].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + maxInclusive := false + logDebugGrammar("FIELD - LESS THAN %f", max) + q := NewNumericRangeInclusiveQuery(nil, &max, nil, &maxInclusive) + q.SetField(field) + yyVAL.q = q + } + case 19: + yyDollar = yyS[yypt-5 : yypt+1] +//line query_string.y:233 + { + field := yyDollar[1].s + max, err := strconv.ParseFloat(yyDollar[5].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("error parsing number: %v", err)) + } + maxInclusive := true + logDebugGrammar("FIELD - LESS THAN OR EQUAL %f", max) + q := NewNumericRangeInclusiveQuery(nil, &max, nil, &maxInclusive) + q.SetField(field) + yyVAL.q = q + } + case 20: + yyDollar = yyS[yypt-4 : yypt+1] +//line query_string.y:246 + { + field := yyDollar[1].s + minInclusive := false + phrase := yyDollar[4].s + + logDebugGrammar("FIELD - GREATER THAN DATE %s", phrase) + minTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(minTime, time.Time{}, &minInclusive, nil) + q.SetField(field) + yyVAL.q = q + } + case 21: + yyDollar = yyS[yypt-5 : yypt+1] +//line query_string.y:261 + { + field := yyDollar[1].s + minInclusive := true + phrase := yyDollar[5].s + + logDebugGrammar("FIELD - GREATER THAN OR EQUAL DATE %s", phrase) + minTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(minTime, time.Time{}, &minInclusive, nil) + q.SetField(field) + yyVAL.q = q + } + case 22: + yyDollar = yyS[yypt-4 : yypt+1] +//line query_string.y:276 + { + field := yyDollar[1].s + maxInclusive := false + phrase := yyDollar[4].s + + logDebugGrammar("FIELD - LESS THAN DATE %s", phrase) + maxTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(time.Time{}, maxTime, nil, &maxInclusive) + q.SetField(field) + yyVAL.q = q + } + case 23: + yyDollar = yyS[yypt-5 : yypt+1] +//line query_string.y:291 + { + field := yyDollar[1].s + maxInclusive := true + phrase := yyDollar[5].s + + logDebugGrammar("FIELD - LESS THAN OR EQUAL DATE %s", phrase) + maxTime, err := queryTimeFromString(phrase) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid time: %v", err)) + } + q := NewDateRangeInclusiveQuery(time.Time{}, maxTime, nil, &maxInclusive) + q.SetField(field) + yyVAL.q = q + } + case 24: + yyDollar = yyS[yypt-0 : yypt+1] +//line query_string.y:307 + { + yyVAL.pf = nil + } + case 25: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:311 + { + yyVAL.pf = nil + boost, err := strconv.ParseFloat(yyDollar[1].s, 64) + if err != nil { + yylex.(*lexerWrapper).lex.Error(fmt.Sprintf("invalid boost value: %v", err)) + } else { + yyVAL.pf = &boost + } + logDebugGrammar("BOOST %f", boost) + } + case 26: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:323 + { + yyVAL.s = yyDollar[1].s + } + case 27: + yyDollar = yyS[yypt-2 : yypt+1] +//line query_string.y:327 + { + yyVAL.s = "-" + yyDollar[2].s + } + case 28: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:332 + { + yyVAL.s = yyDollar[1].s + } + case 29: + yyDollar = yyS[yypt-1 : yypt+1] +//line query_string.y:336 + { + yyVAL.s = yyDollar[1].s + } + } + goto yystack /* stack new state and value */ +} diff --git a/search/query/query_string_lex.go b/search/query/query_string_lex.go new file mode 100644 index 0000000..c01fa6f --- /dev/null +++ b/search/query/query_string_lex.go @@ -0,0 +1,329 @@ +// Copyright (c) 2016 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "bufio" + "io" + "strings" + "unicode" +) + +const reservedChars = "+-=&|>', '<', '=': + l.buf += string(next) + return singleCharOpState, true + case '^': + return inBoostState, true + case '~': + return inTildeState, true + } + + switch { + case !l.inEscape && next == '\\': + l.inEscape = true + return startState, true + case unicode.IsDigit(next): + l.buf += string(next) + return inNumOrStrState, true + case !unicode.IsSpace(next): + l.buf += string(next) + return inStrState, true + } + + // doesn't look like anything, just eat it and stay here + l.reset() + return startState, true +} + +func inPhraseState(l *queryStringLex, next rune, eof bool) (lexState, bool) { + // unterminated phrase eats the phrase + if eof { + l.Error("unterminated quote") + return nil, false + } + + // only a non-escaped " ends the phrase + if !l.inEscape && next == '"' { + // end phrase + l.nextTokenType = tPHRASE + l.nextToken = &yySymType{ + s: l.buf, + } + logDebugTokens("PHRASE - '%s'", l.nextToken.s) + l.reset() + return startState, true + } else if !l.inEscape && next == '\\' { + l.inEscape = true + } else if l.inEscape { + // if in escape, end it + l.inEscape = false + l.buf += unescape(string(next)) + } else { + l.buf += string(next) + } + + return inPhraseState, true +} + +func singleCharOpState(l *queryStringLex, next rune, eof bool) (lexState, bool) { + l.nextToken = &yySymType{} + + switch l.buf { + case "+": + l.nextTokenType = tPLUS + logDebugTokens("PLUS") + case "-": + l.nextTokenType = tMINUS + logDebugTokens("MINUS") + case ":": + l.nextTokenType = tCOLON + logDebugTokens("COLON") + case ">": + l.nextTokenType = tGREATER + logDebugTokens("GREATER") + case "<": + l.nextTokenType = tLESS + logDebugTokens("LESS") + case "=": + l.nextTokenType = tEQUAL + logDebugTokens("EQUAL") + } + + l.reset() + return startState, false +} + +func inBoostState(l *queryStringLex, next rune, eof bool) (lexState, bool) { + + // only a non-escaped space ends the boost (or eof) + if eof || (!l.inEscape && next == ' ') { + // end boost + l.nextTokenType = tBOOST + if l.buf == "" { + l.buf = "1" + } + l.nextToken = &yySymType{ + s: l.buf, + } + logDebugTokens("BOOST - '%s'", l.nextToken.s) + l.reset() + return startState, true + } else if !l.inEscape && next == '\\' { + l.inEscape = true + } else if l.inEscape { + // if in escape, end it + l.inEscape = false + l.buf += unescape(string(next)) + } else { + l.buf += string(next) + } + + return inBoostState, true +} + +func inTildeState(l *queryStringLex, next rune, eof bool) (lexState, bool) { + + // only a non-escaped space ends the tilde (or eof) + if eof || (!l.inEscape && next == ' ') { + // end tilde + l.nextTokenType = tTILDE + if l.buf == "" { + l.buf = "1" + } + l.nextToken = &yySymType{ + s: l.buf, + } + logDebugTokens("TILDE - '%s'", l.nextToken.s) + l.reset() + return startState, true + } else if !l.inEscape && next == '\\' { + l.inEscape = true + } else if l.inEscape { + // if in escape, end it + l.inEscape = false + l.buf += unescape(string(next)) + } else { + l.buf += string(next) + } + + return inTildeState, true +} + +func inNumOrStrState(l *queryStringLex, next rune, eof bool) (lexState, bool) { + // end on non-escaped space, colon, tilde, boost (or eof) + if eof || (!l.inEscape && (next == ' ' || next == ':' || next == '^' || next == '~')) { + // end number + l.nextTokenType = tNUMBER + l.nextToken = &yySymType{ + s: l.buf, + } + logDebugTokens("NUMBER - '%s'", l.nextToken.s) + l.reset() + + consumed := true + if !eof && (next == ':' || next == '^' || next == '~') { + consumed = false + } + + return startState, consumed + } else if !l.inEscape && next == '\\' { + l.inEscape = true + return inNumOrStrState, true + } else if l.inEscape { + // if in escape, end it + l.inEscape = false + l.buf += unescape(string(next)) + // go directly to string, no successfully or unsuccessfully + // escaped string results in a valid number + return inStrState, true + } + + // see where to go + if !l.seenDot && next == '.' { + // stay in this state + l.seenDot = true + l.buf += string(next) + return inNumOrStrState, true + } else if unicode.IsDigit(next) { + l.buf += string(next) + return inNumOrStrState, true + } + + // doesn't look like an number, transition + l.buf += string(next) + return inStrState, true +} + +func inStrState(l *queryStringLex, next rune, eof bool) (lexState, bool) { + // end on non-escaped space, colon, tilde, boost (or eof) + if eof || (!l.inEscape && (next == ' ' || next == ':' || next == '^' || next == '~')) { + // end string + l.nextTokenType = tSTRING + l.nextToken = &yySymType{ + s: l.buf, + } + logDebugTokens("STRING - '%s'", l.nextToken.s) + l.reset() + + consumed := true + if !eof && (next == ':' || next == '^' || next == '~') { + consumed = false + } + + return startState, consumed + } else if !l.inEscape && next == '\\' { + l.inEscape = true + } else if l.inEscape { + // if in escape, end it + l.inEscape = false + l.buf += unescape(string(next)) + } else { + l.buf += string(next) + } + + return inStrState, true +} + +func logDebugTokens(format string, v ...interface{}) { + if debugLexer { + logger.Printf(format, v...) + } +} diff --git a/search/query/query_string_lex_test.go b/search/query/query_string_lex_test.go new file mode 100644 index 0000000..5fd50cf --- /dev/null +++ b/search/query/query_string_lex_test.go @@ -0,0 +1,1230 @@ +package query + +import ( + "reflect" + "strings" + "testing" +) + +func TestLexer(t *testing.T) { + + tests := []struct { + input string + tokens []token + }{ + { + input: "test", + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "test", + }, + }, + }, + }, + { + input: "127.0.0.1", + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "127.0.0.1", + }, + }, + }, + }, + { + input: `"test phrase 1"`, + tokens: []token{ + { + typ: tPHRASE, + lval: yySymType{ + s: "test phrase 1", + }, + }, + }, + }, + { + input: "field:test", + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: "test", + }, + }, + }, + }, + { + input: "field:t-est", + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: "t-est", + }, + }, + }, + }, + { + input: "field:t+est", + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: "t+est", + }, + }, + }, + }, + { + input: "field:t>est", + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: "t>est", + }, + }, + }, + }, + { + input: "field:t5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tGREATER, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:>=5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tGREATER, + }, + { + typ: tEQUAL, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:<5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tLESS, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:<=5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tLESS, + }, + { + typ: tEQUAL, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: "field:-5", + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tMINUS, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:>-5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tGREATER, + }, + { + typ: tMINUS, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:>=-5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tGREATER, + }, + { + typ: tEQUAL, + }, + { + typ: tMINUS, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:<-5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tLESS, + }, + { + typ: tMINUS, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:<=-5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tLESS, + }, + { + typ: tEQUAL, + }, + { + typ: tMINUS, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `field:>"2006-01-02T15:04:05Z"`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tGREATER, + }, + { + typ: tPHRASE, + lval: yySymType{ + s: "2006-01-02T15:04:05Z", + }, + }, + }, + }, + { + input: `field:>="2006-01-02T15:04:05Z"`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tGREATER, + }, + { + typ: tEQUAL, + }, + { + typ: tPHRASE, + lval: yySymType{ + s: "2006-01-02T15:04:05Z", + }, + }, + }, + }, + { + input: `field:<"2006-01-02T15:04:05Z"`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tLESS, + }, + { + typ: tPHRASE, + lval: yySymType{ + s: "2006-01-02T15:04:05Z", + }, + }, + }, + }, + { + input: `field:<="2006-01-02T15:04:05Z"`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "field", + }, + }, + { + typ: tCOLON, + }, + { + typ: tLESS, + }, + { + typ: tEQUAL, + }, + { + typ: tPHRASE, + lval: yySymType{ + s: "2006-01-02T15:04:05Z", + }, + }, + }, + }, + { + input: `/mar.*ty/`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `/mar.*ty/`, + }, + }, + }, + }, + { + input: `name:/mar.*ty/`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "name", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: `/mar.*ty/`, + }, + }, + }, + }, + { + input: `mart*`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `mart*`, + }, + }, + }, + }, + { + input: `name:mart*`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "name", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: `mart*`, + }, + }, + }, + }, + { + input: `name\:marty`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `name:marty`, + }, + }, + }, + }, + { + input: `name:marty\:couchbase`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "name", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: `marty:couchbase`, + }, + }, + }, + }, + { + input: `marty\ couchbase`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `marty couchbase`, + }, + }, + }, + }, + { + input: `\+marty`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `+marty`, + }, + }, + }, + }, + { + input: `\-marty`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `-marty`, + }, + }, + }, + }, + { + input: `"what does \"quote\" mean"`, + tokens: []token{ + { + typ: tPHRASE, + lval: yySymType{ + s: `what does "quote" mean`, + }, + }, + }, + }, + { + input: `can\ i\ escap\e`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `can i escap\e`, + }, + }, + }, + }, + { + input: ` what`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `what`, + }, + }, + }, + }, + { + input: `term^`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `term`, + }, + }, + { + typ: tBOOST, + lval: yySymType{ + s: "1", + }, + }, + }, + }, + { + input: `3.0\:`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `3.0:`, + }, + }, + }, + }, + { + input: `3.0\a`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: `3.0\a`, + }, + }, + }, + }, + { + input: `age:65^10`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "age", + }, + }, + { + typ: tCOLON, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "65", + }, + }, + { + typ: tBOOST, + lval: yySymType{ + s: "10", + }, + }, + }, + }, + { + input: `age:65^10 age:18^5`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "age", + }, + }, + { + typ: tCOLON, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "65", + }, + }, + { + typ: tBOOST, + lval: yySymType{ + s: "10", + }, + }, + { + typ: tSTRING, + lval: yySymType{ + s: "age", + }, + }, + { + typ: tCOLON, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "18", + }, + }, + { + typ: tBOOST, + lval: yySymType{ + s: "5", + }, + }, + }, + }, + { + input: `age:65~2`, + tokens: []token{ + { + typ: tSTRING, + lval: yySymType{ + s: "age", + }, + }, + { + typ: tCOLON, + }, + { + typ: tNUMBER, + lval: yySymType{ + s: "65", + }, + }, + { + typ: tTILDE, + lval: yySymType{ + s: "2", + }, + }, + }, + }, + { + input: `65:cat`, + tokens: []token{ + { + typ: tNUMBER, + lval: yySymType{ + s: "65", + }, + }, + { + typ: tCOLON, + }, + { + typ: tSTRING, + lval: yySymType{ + s: "cat", + }, + }, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + + r := strings.NewReader(test.input) + l := newQueryStringLex(r) + var tokens []token + var lval yySymType + rv := l.Lex(&lval) + for rv > 0 { + //tokenTypes = append(tokenTypes, rv) + tokens = append(tokens, token{typ: rv, lval: lval}) + lval.s = "" + lval.n = 0 + rv = l.Lex(&lval) + } + + if !reflect.DeepEqual(tokens, test.tokens) { + t.Fatalf("\nexpected: %#v\n got: %#v\n", test.tokens, tokens) + } + }) + } +} + +type token struct { + typ int + lval yySymType +} diff --git a/search/query/query_string_parser.go b/search/query/query_string_parser.go new file mode 100644 index 0000000..3fb7731 --- /dev/null +++ b/search/query/query_string_parser.go @@ -0,0 +1,85 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// as of Go 1.8 this requires the goyacc external tool +// available from golang.org/x/tools/cmd/goyacc + +//go:generate goyacc -o query_string.y.go query_string.y +//go:generate sed -i.tmp -e 1d query_string.y.go +//go:generate rm query_string.y.go.tmp + +// note: OSX sed and gnu sed handle the -i (in-place) option differently. +// using -i.tmp works on both, at the expense of having to remove +// the unsightly .tmp files + +package query + +import ( + "fmt" + "strings" +) + +var debugParser bool +var debugLexer bool + +func parseQuerySyntax(query string) (rq Query, err error) { + if query == "" { + return NewMatchNoneQuery(), nil + } + lex := newLexerWrapper(newQueryStringLex(strings.NewReader(query))) + doParse(lex) + + if len(lex.errs) > 0 { + return nil, fmt.Errorf(strings.Join(lex.errs, "\n")) + } + return lex.query, nil +} + +func doParse(lex *lexerWrapper) { + defer func() { + r := recover() + if r != nil { + lex.errs = append(lex.errs, fmt.Sprintf("parse error: %v", r)) + } + }() + + yyParse(lex) +} + +const ( + queryShould = iota + queryMust + queryMustNot +) + +type lexerWrapper struct { + lex yyLexer + errs []string + query *BooleanQuery +} + +func newLexerWrapper(lex yyLexer) *lexerWrapper { + return &lexerWrapper{ + lex: lex, + query: NewBooleanQueryForQueryString(nil, nil, nil), + } +} + +func (l *lexerWrapper) Lex(lval *yySymType) int { + return l.lex.Lex(lval) +} + +func (l *lexerWrapper) Error(s string) { + l.errs = append(l.errs, s) +} diff --git a/search/query/query_string_parser_test.go b/search/query/query_string_parser_test.go new file mode 100644 index 0000000..5231e2d --- /dev/null +++ b/search/query/query_string_parser_test.go @@ -0,0 +1,954 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/mapping" +) + +func TestQuerySyntaxParserValid(t *testing.T) { + thirtyThreePointOh := 33.0 + twoPointOh := 2.0 + fivePointOh := 5.0 + minusFivePointOh := -5.0 + theTruth := true + theFalsehood := false + theDate, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + if err != nil { + t.Fatal(err) + } + tests := []struct { + input string + result Query + mapping mapping.IndexMapping + }{ + { + input: "test", + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery("test"), + }, + nil), + }, + { + input: "127.0.0.1", + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery("127.0.0.1"), + }, + nil), + }, + { + input: `"test phrase 1"`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchPhraseQuery("test phrase 1"), + }, + nil), + }, + { + input: "field:test", + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchQuery("test") + q.SetField("field") + return q + }(), + }, + nil), + }, + // - is allowed inside a term, just not the start + { + input: "field:t-est", + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchQuery("t-est") + q.SetField("field") + return q + }(), + }, + nil), + }, + // + is allowed inside a term, just not the start + { + input: "field:t+est", + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchQuery("t+est") + q.SetField("field") + return q + }(), + }, + nil), + }, + // > is allowed inside a term, just not the start + { + input: "field:t>est", + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchQuery("t>est") + q.SetField("field") + return q + }(), + }, + nil), + }, + // < is allowed inside a term, just not the start + { + input: "field:t5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(&fivePointOh, nil, &theFalsehood, nil) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:>=5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(&fivePointOh, nil, &theTruth, nil) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:<5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(nil, &fivePointOh, nil, &theFalsehood) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:<=5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(nil, &fivePointOh, nil, &theTruth) + q.SetField("field") + return q + }(), + }, + nil), + }, + // new range tests with negative number + { + input: "field:-5", + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + qo := NewDisjunctionQuery( + []Query{ + func() Query { + q := NewMatchQuery("-5") + q.SetField("field") + return q + }(), + func() Query { + q := NewNumericRangeInclusiveQuery(&minusFivePointOh, &minusFivePointOh, &theTruth, &theTruth) + q.SetField("field") + return q + }(), + }) + qo.queryStringMode = true + return qo + }(), + }, + nil), + }, + { + input: `field:>-5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(&minusFivePointOh, nil, &theFalsehood, nil) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:>=-5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(&minusFivePointOh, nil, &theTruth, nil) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:<-5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(nil, &minusFivePointOh, nil, &theFalsehood) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:<=-5`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewNumericRangeInclusiveQuery(nil, &minusFivePointOh, nil, &theTruth) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:>"2006-01-02T15:04:05Z"`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewDateRangeInclusiveQuery(theDate, time.Time{}, &theFalsehood, nil) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:>="2006-01-02T15:04:05Z"`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewDateRangeInclusiveQuery(theDate, time.Time{}, &theTruth, nil) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:<"2006-01-02T15:04:05Z"`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewDateRangeInclusiveQuery(time.Time{}, theDate, nil, &theFalsehood) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `field:<="2006-01-02T15:04:05Z"`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewDateRangeInclusiveQuery(time.Time{}, theDate, nil, &theTruth) + q.SetField("field") + return q + }(), + }, + nil), + }, + { + input: `/mar.*ty/`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewRegexpQuery("mar.*ty"), + }, + nil), + }, + { + input: `name:/mar.*ty/`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewRegexpQuery("mar.*ty") + q.SetField("name") + return q + }(), + }, + nil), + }, + { + input: `mart*`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewWildcardQuery("mart*"), + }, + nil), + }, + { + input: `name:mart*`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewWildcardQuery("mart*") + q.SetField("name") + return q + }(), + }, + nil), + }, + + // tests for escaping + + // escape : as field delimeter + { + input: `name\:marty`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery("name:marty"), + }, + nil), + }, + // first colon delimiter, second escaped + { + input: `name:marty\:couchbase`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchQuery("marty:couchbase") + q.SetField("name") + return q + }(), + }, + nil), + }, + // escape space, single arguemnt to match query + { + input: `marty\ couchbase`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery("marty couchbase"), + }, + nil), + }, + // escape leading plus, not a must clause + { + input: `\+marty`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery("+marty"), + }, + nil), + }, + // escape leading minus, not a must not clause + { + input: `\-marty`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery("-marty"), + }, + nil), + }, + // escape quote inside of phrase + { + input: `"what does \"quote\" mean"`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchPhraseQuery(`what does "quote" mean`), + }, + nil), + }, + // escaping an unsupported character retains backslash + { + input: `can\ i\ escap\e`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery(`can i escap\e`), + }, + nil), + }, + // leading spaces + { + input: ` what`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery(`what`), + }, + nil), + }, + // no boost value defaults to 1 + { + input: `term^`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchQuery(`term`) + q.SetBoost(1.0) + return q + }(), + }, + nil), + }, + // weird lexer cases, something that starts like a number + // but contains escape and ends up as string + { + input: `3.0\:`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery(`3.0:`), + }, + nil), + }, + { + input: `3.0\a`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + NewMatchQuery(`3.0\a`), + }, + nil), + }, + + // field names as phrases + { + input: `"fie ld":test`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchQuery("test") + q.SetField("fie ld") + return q + }(), + }, + nil), + }, + { + input: `"fie ld":"test"`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewMatchPhraseQuery("test") + q.SetField("fie ld") + return q + }(), + }, + nil), + }, + // exact match number with boost + { + input: `age:65^10`, + mapping: mapping.NewIndexMapping(), + result: NewBooleanQueryForQueryString( + nil, + []Query{ + func() Query { + q := NewDisjunctionQuery([]Query{ + func() Query { + mq := NewMatchQuery("65") + mq.SetField("age") + return mq + }(), + func() Query { + val := float64(65) + inclusive := true + nq := NewNumericRangeInclusiveQuery(&val, &val, &inclusive, &inclusive) + nq.SetField("age") + return nq + }(), + }) + q.SetBoost(10) + q.queryStringMode = true + return q + }(), + }, + nil), + }, + } + + // turn on lexer debugging + // debugLexer = true + // debugParser = true + // logger = log.New(os.Stderr, "bleve ", log.LstdFlags) + + for _, test := range tests { + + q, err := parseQuerySyntax(test.input) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(q, test.result) { + t.Errorf("Expected %#v, got %#v: for %s", test.result, q, test.input) + } + } +} + +func TestQuerySyntaxParserInvalid(t *testing.T) { + tests := []struct { + input string + }{ + {"^"}, + {"^5"}, + {"field:-text"}, + {"field:+text"}, + {"field:>text"}, + {"field:>=text"}, + {"field:99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999`}, + {`field:>=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999`}, + {`field:<99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999`}, + {`field:<=99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999`}, + } + + // turn on lexer debugging + // debugLexer = true + // logger = log.New(os.Stderr, "bleve", log.LstdFlags) + + for _, test := range tests { + _, err := parseQuerySyntax(test.input) + if err == nil { + t.Errorf("expected error, got nil for `%s`", test.input) + } + } +} + +func BenchmarkLexer(b *testing.B) { + for n := 0; n < b.N; n++ { + var tokenTypes []int + var tokens []yySymType + r := strings.NewReader(`+field4:"test phrase 1"`) + l := newQueryStringLex(r) + var lval yySymType + rv := l.Lex(&lval) + + for rv > 0 { + tokenTypes = append(tokenTypes, rv) + tokens = append(tokens, lval) + + // use the slice to silence the compiler warning + _ = tokenTypes + _ = tokens + + lval.s = "" + lval.n = 0 + rv = l.Lex(&lval) + } + } +} diff --git a/search/query/query_test.go b/search/query/query_test.go new file mode 100644 index 0000000..60c1fa3 --- /dev/null +++ b/search/query/query_test.go @@ -0,0 +1,1042 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "reflect" + "sort" + "strings" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/mapping" +) + +var minNum = 5.1 +var maxNum = 7.1 +var minTerm = "bob" +var maxTerm = "cat" +var startDateStr = "2011-01-01T00:00:00Z" +var endDateStr = "2012-01-01T00:00:00Z" +var startDate time.Time +var endDate time.Time + +func init() { + var err error + startDate, err = time.Parse(time.RFC3339, startDateStr) + if err != nil { + panic(err) + } + endDate, err = time.Parse(time.RFC3339, endDateStr) + if err != nil { + panic(err) + } +} + +func TestParseQuery(t *testing.T) { + tests := []struct { + input []byte + output Query + err bool + }{ + { + input: []byte(`{"term":"water","field":"desc"}`), + output: func() Query { + q := NewTermQuery("water") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc"}`), + output: func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), + output: func() Query { + q := NewMatchQuery("beer") + q.SetOperator(MatchQueryOperatorAnd) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"and"}`), + output: func() Query { + operator := MatchQueryOperatorAnd + q := NewMatchQuery("beer") + q.SetOperator(operator) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: func() Query { + q := NewMatchQuery("beer") + q.SetOperator(MatchQueryOperatorOr) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"or"}`), + output: func() Query { + operator := MatchQueryOperatorOr + q := NewMatchQuery("beer") + q.SetOperator(operator) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match":"beer","field":"desc","operator":"does not exist"}`), + output: nil, + err: true, + }, + { + input: []byte(`{"match_phrase":"light beer","field":"desc"}`), + output: func() Query { + q := NewMatchPhraseQuery("light beer") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"must":{"conjuncts": [{"match":"beer","field":"desc"}]},"should":{"disjuncts": [{"match":"water","field":"desc"}],"min":1.0},"must_not":{"disjuncts": [{"match":"devon","field":"desc"}]}}`), + output: func() Query { + q := NewBooleanQuery( + []Query{func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("water") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("devon") + q.SetField("desc") + return q + }()}) + q.SetMinShould(1) + return q + }(), + }, + { + input: []byte(`{"terms":["watered","down"],"field":"desc"}`), + output: NewPhraseQuery([]string{"watered", "down"}, "desc"), + }, + { + input: []byte(`{"query":"+beer \"light beer\" -devon"}`), + output: NewQueryStringQuery(`+beer "light beer" -devon`), + }, + { + input: []byte(`{"min":5.1,"max":7.1,"field":"desc"}`), + output: func() Query { + q := NewNumericRangeQuery(&minNum, &maxNum) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"min":"bob","max":"cat","field":"desc"}`), + output: func() Query { + q := NewTermRangeQuery(minTerm, maxTerm) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`), + output: func() Query { + q := NewDateRangeStringQuery(startDateStr, endDateStr) + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"prefix":"budwei","field":"desc"}`), + output: func() Query { + q := NewPrefixQuery("budwei") + q.SetField("desc") + return q + }(), + }, + { + input: []byte(`{"match_all":{}}`), + output: NewMatchAllQuery(), + }, + { + input: []byte(`{"match_none":{}}`), + output: NewMatchNoneQuery(), + }, + { + input: []byte(`{"ids":["a","b","c"]}`), + output: NewDocIDQuery([]string{"a", "b", "c"}), + }, + { + input: []byte(`{"bool": true}`), + output: NewBoolFieldQuery(true), + }, + { + input: []byte(`{"field": "x", "cidr": "1.2.3.0/4"}`), + output: func() Query { + q := NewIPRangeQuery("1.2.3.0/4") + q.SetField("x") + return q + }(), + }, + { + input: []byte(`{"madeitup":"queryhere"}`), + output: nil, + err: true, + }, + } + + for i, test := range tests { + actual, err := ParseQuery(test.input) + if err != nil && test.err == false { + t.Errorf("error %v for %d", err, i) + } + + if !reflect.DeepEqual(test.output, actual) { + t.Errorf("expected: %#v, got: %#v for %s", test.output, actual, string(test.input)) + } + } +} + +func TestQueryValidate(t *testing.T) { + tests := []struct { + query Query + err bool + }{ + { + query: func() Query { + q := NewTermQuery("water") + q.SetField("desc") + return q + }(), + }, + { + query: func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }(), + }, + { + query: func() Query { + q := NewMatchPhraseQuery("light beer") + q.SetField("desc") + return q + }(), + }, + { + query: func() Query { + q := NewNumericRangeQuery(&minNum, &maxNum) + q.SetField("desc") + return q + }(), + }, + { + query: func() Query { + q := NewNumericRangeQuery(nil, nil) + q.SetField("desc") + return q + }(), + err: true, + }, + { + query: func() Query { + q := NewDateRangeQuery(startDate, endDate) + q.SetField("desc") + return q + }(), + }, + { + query: func() Query { + q := NewPrefixQuery("budwei") + q.SetField("desc") + return q + }(), + }, + { + query: NewQueryStringQuery(`+beer "light beer" -devon`), + }, + { + query: NewPhraseQuery([]string{"watered", "down"}, "desc"), + }, + { + query: NewPhraseQuery([]string{}, "field"), + err: true, + }, + { + query: func() Query { + q := NewMatchNoneQuery() + q.SetBoost(25) + return q + }(), + }, + { + query: func() Query { + q := NewMatchAllQuery() + q.SetBoost(25) + return q + }(), + }, + { + query: NewBooleanQuery( + []Query{func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("water") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("devon") + q.SetField("desc") + return q + }()}), + }, + { + query: NewBooleanQuery( + nil, + nil, + []Query{func() Query { + q := NewMatchQuery("devon") + q.SetField("desc") + return q + }()}), + }, + { + query: NewBooleanQuery( + []Query{}, + []Query{}, + []Query{func() Query { + q := NewMatchQuery("devon") + q.SetField("desc") + return q + }()}), + }, + { + query: NewBooleanQuery( + nil, + nil, + nil), + err: true, + }, + { + query: NewBooleanQuery( + []Query{}, + []Query{}, + []Query{}), + err: true, + }, + { + query: func() Query { + q := NewBooleanQuery( + []Query{func() Query { + q := NewMatchQuery("beer") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("water") + q.SetField("desc") + return q + }()}, + []Query{func() Query { + q := NewMatchQuery("devon") + q.SetField("desc") + return q + }()}) + q.SetMinShould(2) + return q + }(), + err: true, + }, + { + query: func() Query { + q := NewDocIDQuery(nil) + q.SetBoost(25) + return q + }(), + }, + } + + for _, test := range tests { + if vq, ok := test.query.(ValidatableQuery); ok { + actual := vq.Validate() + if actual != nil && !test.err { + t.Errorf("expected no error: %#v got %#v", test.err, actual) + } else if actual == nil && test.err { + t.Errorf("expected error: %#v got %#v", test.err, actual) + } + } + } +} + +func TestDumpQuery(t *testing.T) { + mapping := mapping.NewIndexMapping() + q := NewQueryStringQuery("+water -light beer") + s, err := DumpQuery(mapping, q) + if err != nil { + t.Fatal(err) + } + s = strings.TrimSpace(s) + wanted := strings.TrimSpace(`{ + "must": { + "conjuncts": [ + { + "match": "water", + "prefix_length": 0, + "fuzziness": 0 + } + ] + }, + "should": { + "disjuncts": [ + { + "match": "beer", + "prefix_length": 0, + "fuzziness": 0 + } + ], + "min": 0 + }, + "must_not": { + "disjuncts": [ + { + "match": "light", + "prefix_length": 0, + "fuzziness": 0 + } + ], + "min": 0 + } +}`) + if wanted != s { + t.Fatalf("query:\n%s\ndiffers from expected:\n%s", s, wanted) + } +} + +func TestGeoShapeQuery(t *testing.T) { + tests := []struct { + input []byte + output Query + err bool + }{ + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "polygon", + "coordinates": [[ + [ + 74.1357421875, + 30.600093873550072 + ], + [ + 67.0166015625, + 21.57571893245848 + ], + [ + 68.8623046875, + 9.145486056167277 + ], + [ + 83.1884765625, + 4.083452772038619 + ], + [ + 88.9892578125, + 22.67484735118852 + ], + [ + 74.1357421875, + 30.600093873550072 + ]]] + }, + "relation": "intersects" + }}`), + output: func() Query { + q, _ := NewGeoShapeQuery([][][][]float64{{{{74.1357421875, 30.600093873550072}, + {67.0166015625, 21.57571893245848}, {68.8623046875, 9.145486056167277}, + {83.1884765625, 4.083452772038619}, {88.9892578125, 22.67484735118852}, + {74.1357421875, 30.600093873550072}}}}, geo.PolygonType, "intersects") + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "multipolygon", + "coordinates": [ + [[ + [ + 77.58268117904663, + 12.980513152175025 + ], + [ + 77.58147954940794, + 12.977983107483992 + ], + [ + 77.58708000183104, + 12.97886130773254 + ], + [ + 77.58268117904663, + 12.980513152175025 + ] + ]], + [[ + [ + 77.5864577293396, + 12.97762764459667 + ], + [ + 77.58879661560059, + 12.975076660730531 + ], + [ + 77.59115695953369, + 12.979216768855913 + ], + [ + 77.5864577293396, + 12.97762764459667 + ] + ]] + ] + }, + "relation": "contains" + }}`), + output: func() Query { + q, _ := NewGeoShapeQuery([][][][]float64{ + {{{77.58268117904663, 12.980513152175025}, + {77.58147954940794, 12.977983107483992}, {77.58708000183104, 12.97886130773254}, + {77.58268117904663, 12.980513152175025}}}, + {{{77.5864577293396, 12.97762764459667}, {77.58879661560059, 12.975076660730531}, + {77.59115695953369, 12.979216768855913}, {77.5864577293396, 12.97762764459667}}}}, + geo.MultiPolygonType, "contains") + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "point", + "coordinates": [77.58268117904663, 12.980513152175025] + }, + "relation": "contains" + }}`), + output: func() Query { + q, _ := NewGeoShapeQuery([][][][]float64{ + {{{77.58268117904663, 12.980513152175025}}}}, + geo.PointType, "contains") + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "multipoint", + "coordinates": [[77.58268117904663, 12.980513152175025], + [77.5864577293396, 12.97762764459667]] + }, + "relation": "intersects" + }}`), + output: func() Query { + q, _ := NewGeoShapeQuery([][][][]float64{ + {{{77.58268117904663, 12.980513152175025}, + {77.5864577293396, 12.97762764459667}}}}, + geo.MultiPointType, "intersects") + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "linestring", + "coordinates": [[77.58268117904663, 12.980513152175025], + [77.5864577293396, 12.97762764459667]] + }, + "relation": "intersects" + }}`), + output: func() Query { + q, _ := NewGeoShapeQuery([][][][]float64{ + {{{77.58268117904663, 12.980513152175025}, + {77.5864577293396, 12.97762764459667}}}}, + geo.LineStringType, "intersects") + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "multilinestring", + "coordinates": [ + [[77.58268117904663, 12.980513152175025], + [77.5864577293396, 12.97762764459667]], + [[77.5864577293396,12.97762764459667], + [77.58879661560059, 12.975076660730531]]] + }, + "relation": "intersects" + }}`), + output: func() Query { + q, _ := NewGeoShapeQuery([][][][]float64{{ + {{77.58268117904663, 12.980513152175025}, + {77.5864577293396, 12.97762764459667}}, + {{77.5864577293396, 12.97762764459667}, + {77.58879661560059, 12.975076660730531}}}}, + geo.MultiLineStringType, "intersects") + + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "envelope", + "coordinates": [[77.58268117904663, 12.980513152175025], + [77.5864577293396, 12.97762764459667]] + }, + "relation": "within" + }}`), + output: func() Query { + q, _ := NewGeoShapeQuery([][][][]float64{{ + {{77.58268117904663, 12.980513152175025}, + {77.5864577293396, 12.97762764459667}}}}, + geo.EnvelopeType, "within") + + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "circle", + "coordinates": [77.58268117904663, 12.980513152175025], + "radius": "100m" + }, + "relation": "within" + }}`), + output: func() Query { + q, _ := NewGeoShapeCircleQuery([]float64{ + 77.58268117904663, 12.980513152175025}, + "100m", "within") + + q.SetField("region") + return q + }(), + }, + { + input: []byte(`{ + "field" : "region", + "geometry": { + "shape": { + "type": "geometrycollection", + "geometries": [ + { + "type": "point", + "coordinates": [ + 77.59158611297607, + 12.972002899506203 + ] + }, + { + "type": "linestring", + "coordinates": [ + [ + 77.58851766586304, + 12.973152950670608 + ], + [ + 77.58937597274779, + 12.972212000113458 + ] + ] + }, + { + "type": "polygon", + "coordinates": [ + [ + [ + 77.59055614471436, + 12.974721193688106 + ], + [ + 77.58954763412476, + 12.97350841995465 + ], + [ + 77.59141445159912, + 12.973382960265356 + ], + [ + 77.59055614471436, + 12.974721193688106 + ] + ] + ] + } + ] + }, + "relation": "contains" + }}`), + output: func() Query { + q, _ := NewGeometryCollectionQuery([][][][][]float64{ + {{{{77.59158611297607, 12.972002899506203}}}}, + {{{{77.58851766586304, 12.973152950670608}, {77.58937597274779, 12.972212000113458}}}}, + {{{{77.59055614471436, 12.974721193688106}, {77.58954763412476, 12.97350841995465}, + {77.59141445159912, 12.973382960265356}, {77.59055614471436, 12.974721193688106}}}}, + }, + []string{"point", "linestring", "polygon"}, "contains") + q.SetField("region") + return q + }(), + }, + } + + for i, test := range tests { + actual, err := ParseQuery(test.input) + if err != nil && test.err == false { + t.Errorf("error %v for %d", err, i) + } + + if !reflect.DeepEqual(test.output, actual) { + t.Errorf("expected: %#v, got: %#v for %s", test.output, actual, string(test.input)) + } + } +} + +func TestParseEmptyQuery(t *testing.T) { + var qBytes []byte + rv, err := ParseQuery(qBytes) + if err != nil { + t.Fatal(err) + } + expect := NewMatchNoneQuery() + if !reflect.DeepEqual(rv, expect) { + t.Errorf("[1] Expected %#v, got %#v", expect, rv) + } + + qBytes = []byte(`{}`) + rv, err = ParseQuery(qBytes) + if err != nil { + t.Fatal(err) + } + expect = NewMatchNoneQuery() + if !reflect.DeepEqual(rv, expect) { + t.Errorf("[2] Expected %#v, got %#v", expect, rv) + } +} + +func TestExtractFields(t *testing.T) { + testQueries := []struct { + query string + expFields []string + }{ + { + query: `{"term":"water","field":"desc"}`, + expFields: []string{"desc"}, + }, + { + query: `{ + "must": { + "conjuncts": [ + { + "match": "water", + "prefix_length": 0, + "fuzziness": 0 + } + ] + }, + "should": { + "disjuncts": [ + { + "match": "beer", + "prefix_length": 0, + "fuzziness": 0 + } + ], + "min": 0 + }, + "must_not": { + "disjuncts": [ + { + "match": "light", + "prefix_length": 0, + "fuzziness": 0 + } + ], + "min": 0 + } + }`, + expFields: []string{"_all"}, + }, + { + query: `{ + "must": { + "conjuncts": [ + { + "match": "water", + "prefix_length": 0, + "field": "desc", + "fuzziness": 0 + } + ] + }, + "should": { + "disjuncts": [ + { + "match": "beer", + "prefix_length": 0, + "field": "desc", + "fuzziness": 0 + } + ], + "min": 0 + }, + "must_not": { + "disjuncts": [ + { + "match": "light", + "prefix_length": 0, + "field": "genre", + "fuzziness": 0 + } + ], + "min": 0 + } + }`, + expFields: []string{"desc", "genre"}, + }, + { + query: ` + { + "conjuncts": [ + { + "conjuncts": [ + { + "conjuncts": [ + { + "conjuncts": [ + { + "field": "date", + "start": "2002-09-05T08:09:00Z", + "end": "2007-03-01T03:52:00Z", + "inclusive_start": true, + "inclusive_end": true + }, + { + "field": "number", + "min": 1260295, + "max": 3917314, + "inclusive_min": true, + "inclusive_max": true + } + ] + }, + { + "conjuncts": [ + { + "field": "date2", + "start": "2004-08-21T18:30:00Z", + "end": "2006-03-24T08:08:00Z", + "inclusive_start": true, + "inclusive_end": true + }, + { + "field": "number", + "min": 165449, + "max": 3847517, + "inclusive_min": true, + "inclusive_max": true + } + ] + } + ] + }, + { + "conjuncts": [ + { + "conjuncts": [ + { + "field": "date", + "start": "2004-09-02T22:15:00Z", + "end": "2008-06-22T15:06:00Z", + "inclusive_start": true, + "inclusive_end": true + }, + { + "field": "number2", + "min": 876843, + "max": 3363351, + "inclusive_min": true, + "inclusive_max": true + } + ] + }, + { + "conjuncts": [ + { + "field": "date", + "start": "2000-12-03T21:35:00Z", + "end": "2008-02-07T05:00:00Z", + "inclusive_start": true, + "inclusive_end": true + }, + { + "field": "number", + "min": 2021479, + "max": 4763404, + "inclusive_min": true, + "inclusive_max": true + } + ] + } + ] + } + ] + }, + { + "conjuncts": [ + { + "conjuncts": [ + { + "field": "date3", + "start": "2000-03-13T07:13:00Z", + "end": "2005-09-19T09:33:00Z", + "inclusive_start": true, + "inclusive_end": true + }, + { + "field": "number", + "min": 883125, + "max": 4817433, + "inclusive_min": true, + "inclusive_max": true + } + ] + }, + { + "conjuncts": [ + { + "field": "date", + "start": "2002-08-10T22:42:00Z", + "end": "2008-02-10T23:19:00Z", + "inclusive_start": true, + "inclusive_end": true + }, + { + "field": "number", + "min": 896115, + "max": 3897074, + "inclusive_min": true, + "inclusive_max": true + } + ] + } + ] + } + ] + }`, + expFields: []string{"date", "number", "date2", "number2", "date3"}, + }, + { + query: `{ + "query" : "hardworking people" + }`, + expFields: []string{"_all"}, + }, + { + query: `{ + "query" : "text:hardworking people" + }`, + expFields: []string{"text", "_all"}, + }, + { + query: `{ + "query" : "text:\"hardworking people\"" + }`, + expFields: []string{"text"}, + }, + } + + m := mapping.NewIndexMapping() + for i, test := range testQueries { + q, err := ParseQuery([]byte(test.query)) + if err != nil { + t.Fatal(err) + } + fields, err := ExtractFields(q, m, nil) + if err != nil { + t.Fatal(err) + } + var fieldsSlice []string + for k := range fields { + fieldsSlice = append(fieldsSlice, k) + } + sort.Strings(test.expFields) + sort.Strings(fieldsSlice) + if !reflect.DeepEqual(fieldsSlice, test.expFields) { + t.Errorf("Test %d: expected %v, got %v", i, test.expFields, fieldsSlice) + } + } +} diff --git a/search/query/regexp.go b/search/query/regexp.go new file mode 100644 index 0000000..189fd5f --- /dev/null +++ b/search/query/regexp.go @@ -0,0 +1,79 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "strings" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type RegexpQuery struct { + Regexp string `json:"regexp"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewRegexpQuery creates a new Query which finds +// documents containing terms that match the +// specified regular expression. The regexp pattern +// SHOULD NOT include ^ or $ modifiers, the search +// will only match entire terms even without them. +func NewRegexpQuery(regexp string) *RegexpQuery { + return &RegexpQuery{ + Regexp: regexp, + } +} + +func (q *RegexpQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *RegexpQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *RegexpQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *RegexpQuery) Field() string { + return q.FieldVal +} + +func (q *RegexpQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + // require that pattern NOT be anchored to start and end of term. + // do not attempt to remove trailing $, its presence is not + // known to interfere with LiteralPrefix() the way ^ does + // and removing $ introduces possible ambiguities with escaped \$, \\$, etc + actualRegexp := q.Regexp + actualRegexp = strings.TrimPrefix(actualRegexp, "^") // remove leading ^ if it exists + + return searcher.NewRegexpStringSearcher(ctx, i, actualRegexp, field, q.BoostVal.Value(), options) +} + +func (q *RegexpQuery) Validate() error { + return nil // real validation delayed until searcher constructor +} diff --git a/search/query/term.go b/search/query/term.go new file mode 100644 index 0000000..5c6af39 --- /dev/null +++ b/search/query/term.go @@ -0,0 +1,63 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type TermQuery struct { + Term string `json:"term"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewTermQuery creates a new Query for finding an +// exact term match in the index. +func NewTermQuery(term string) *TermQuery { + return &TermQuery{ + Term: term, + } +} + +func (q *TermQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *TermQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *TermQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *TermQuery) Field() string { + return q.FieldVal +} + +func (q *TermQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + return searcher.NewTermSearcher(ctx, i, q.Term, field, q.BoostVal.Value(), options) +} diff --git a/search/query/term_range.go b/search/query/term_range.go new file mode 100644 index 0000000..4dc3a34 --- /dev/null +++ b/search/query/term_range.go @@ -0,0 +1,96 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +type TermRangeQuery struct { + Min string `json:"min,omitempty"` + Max string `json:"max,omitempty"` + InclusiveMin *bool `json:"inclusive_min,omitempty"` + InclusiveMax *bool `json:"inclusive_max,omitempty"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewTermRangeQuery creates a new Query for ranges +// of text term values. +// Either, but not both endpoints can be nil. +// The minimum value is inclusive. +// The maximum value is exclusive. +func NewTermRangeQuery(min, max string) *TermRangeQuery { + return NewTermRangeInclusiveQuery(min, max, nil, nil) +} + +// NewTermRangeInclusiveQuery creates a new Query for ranges +// of numeric values. +// Either, but not both endpoints can be nil. +// Control endpoint inclusion with inclusiveMin, inclusiveMax. +func NewTermRangeInclusiveQuery(min, max string, minInclusive, maxInclusive *bool) *TermRangeQuery { + return &TermRangeQuery{ + Min: min, + Max: max, + InclusiveMin: minInclusive, + InclusiveMax: maxInclusive, + } +} + +func (q *TermRangeQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *TermRangeQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *TermRangeQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *TermRangeQuery) Field() string { + return q.FieldVal +} + +func (q *TermRangeQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + var minTerm []byte + if q.Min != "" { + minTerm = []byte(q.Min) + } + var maxTerm []byte + if q.Max != "" { + maxTerm = []byte(q.Max) + } + return searcher.NewTermRangeSearcher(ctx, i, minTerm, maxTerm, q.InclusiveMin, q.InclusiveMax, field, q.BoostVal.Value(), options) +} + +func (q *TermRangeQuery) Validate() error { + if q.Min == "" && q.Min == q.Max { + return fmt.Errorf("term range query must specify min or max") + } + return nil +} diff --git a/search/query/wildcard.go b/search/query/wildcard.go new file mode 100644 index 0000000..f04f3f2 --- /dev/null +++ b/search/query/wildcard.go @@ -0,0 +1,94 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package query + +import ( + "context" + "strings" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/searcher" + index "github.com/blevesearch/bleve_index_api" +) + +var wildcardRegexpReplacer = strings.NewReplacer( + // characters in the wildcard that must + // be escaped in the regexp + "+", `\+`, + "(", `\(`, + ")", `\)`, + "^", `\^`, + "$", `\$`, + ".", `\.`, + "{", `\{`, + "}", `\}`, + "[", `\[`, + "]", `\]`, + `|`, `\|`, + `\`, `\\`, + // wildcard characters + "*", ".*", + "?", ".") + +type WildcardQuery struct { + Wildcard string `json:"wildcard"` + FieldVal string `json:"field,omitempty"` + BoostVal *Boost `json:"boost,omitempty"` +} + +// NewWildcardQuery creates a new Query which finds +// documents containing terms that match the +// specified wildcard. In the wildcard pattern '*' +// will match any sequence of 0 or more characters, +// and '?' will match any single character. +func NewWildcardQuery(wildcard string) *WildcardQuery { + return &WildcardQuery{ + Wildcard: wildcard, + } +} + +func (q *WildcardQuery) SetBoost(b float64) { + boost := Boost(b) + q.BoostVal = &boost +} + +func (q *WildcardQuery) Boost() float64 { + return q.BoostVal.Value() +} + +func (q *WildcardQuery) SetField(f string) { + q.FieldVal = f +} + +func (q *WildcardQuery) Field() string { + return q.FieldVal +} + +func (q *WildcardQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { + field := q.FieldVal + if q.FieldVal == "" { + field = m.DefaultSearchField() + } + + regexpString := wildcardRegexpReplacer.Replace(q.Wildcard) + + return searcher.NewRegexpStringSearcher(ctx, i, regexpString, field, + q.BoostVal.Value(), options) +} + +func (q *WildcardQuery) Validate() error { + return nil // real validation delayed until searcher constructor +} diff --git a/search/scorer/scorer_conjunction.go b/search/scorer/scorer_conjunction.go new file mode 100644 index 0000000..f3c81a7 --- /dev/null +++ b/search/scorer/scorer_conjunction.go @@ -0,0 +1,72 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorer + +import ( + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" +) + +var reflectStaticSizeConjunctionQueryScorer int + +func init() { + var cqs ConjunctionQueryScorer + reflectStaticSizeConjunctionQueryScorer = int(reflect.TypeOf(cqs).Size()) +} + +type ConjunctionQueryScorer struct { + options search.SearcherOptions +} + +func (s *ConjunctionQueryScorer) Size() int { + return reflectStaticSizeConjunctionQueryScorer + size.SizeOfPtr +} + +func NewConjunctionQueryScorer(options search.SearcherOptions) *ConjunctionQueryScorer { + return &ConjunctionQueryScorer{ + options: options, + } +} + +func (s *ConjunctionQueryScorer) Score(ctx *search.SearchContext, constituents []*search.DocumentMatch) *search.DocumentMatch { + var sum float64 + var childrenExplanations []*search.Explanation + if s.options.Explain { + childrenExplanations = make([]*search.Explanation, len(constituents)) + } + + for i, docMatch := range constituents { + sum += docMatch.Score + if s.options.Explain { + childrenExplanations[i] = docMatch.Expl + } + } + newScore := sum + var newExpl *search.Explanation + if s.options.Explain { + newExpl = &search.Explanation{Value: sum, Message: "sum of:", Children: childrenExplanations} + } + + // reuse constituents[0] as the return value + rv := constituents[0] + rv.Score = newScore + rv.Expl = newExpl + rv.FieldTermLocations = search.MergeFieldTermLocations( + rv.FieldTermLocations, constituents[1:]) + + return rv +} diff --git a/search/scorer/scorer_constant.go b/search/scorer/scorer_constant.go new file mode 100644 index 0000000..c030b85 --- /dev/null +++ b/search/scorer/scorer_constant.go @@ -0,0 +1,132 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorer + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeConstantScorer int + +func init() { + var cs ConstantScorer + reflectStaticSizeConstantScorer = int(reflect.TypeOf(cs).Size()) +} + +type ConstantScorer struct { + constant float64 + boost float64 + options search.SearcherOptions + queryNorm float64 + queryWeight float64 + queryWeightExplanation *search.Explanation + includeScore bool +} + +func (s *ConstantScorer) Size() int { + sizeInBytes := reflectStaticSizeConstantScorer + size.SizeOfPtr + + if s.queryWeightExplanation != nil { + sizeInBytes += s.queryWeightExplanation.Size() + } + + return sizeInBytes +} + +func NewConstantScorer(constant float64, boost float64, options search.SearcherOptions) *ConstantScorer { + rv := ConstantScorer{ + options: options, + queryWeight: 1.0, + constant: constant, + boost: boost, + includeScore: options.Score != "none", + } + + return &rv +} + +func (s *ConstantScorer) Weight() float64 { + sum := s.boost + return sum * sum +} + +func (s *ConstantScorer) SetQueryNorm(qnorm float64) { + s.queryNorm = qnorm + + // update the query weight + s.queryWeight = s.boost * s.queryNorm + + if s.options.Explain { + childrenExplanations := make([]*search.Explanation, 2) + childrenExplanations[0] = &search.Explanation{ + Value: s.boost, + Message: "boost", + } + childrenExplanations[1] = &search.Explanation{ + Value: s.queryNorm, + Message: "queryNorm", + } + s.queryWeightExplanation = &search.Explanation{ + Value: s.queryWeight, + Message: fmt.Sprintf("ConstantScore()^%f, product of:", s.boost), + Children: childrenExplanations, + } + } +} + +func (s *ConstantScorer) Score(ctx *search.SearchContext, id index.IndexInternalID) *search.DocumentMatch { + var scoreExplanation *search.Explanation + + rv := ctx.DocumentMatchPool.Get() + rv.IndexInternalID = id + + if s.includeScore { + score := s.constant + + if s.options.Explain { + scoreExplanation = &search.Explanation{ + Value: score, + Message: "ConstantScore()", + } + } + + // if the query weight isn't 1, multiply + if s.queryWeight != 1.0 { + score = score * s.queryWeight + if s.options.Explain { + childExplanations := make([]*search.Explanation, 2) + childExplanations[0] = s.queryWeightExplanation + childExplanations[1] = scoreExplanation + scoreExplanation = &search.Explanation{ + Value: score, + Message: fmt.Sprintf("weight(^%f), product of:", s.boost), + Children: childExplanations, + } + } + } + + rv.Score = score + if s.options.Explain { + rv.Expl = scoreExplanation + } + } + + return rv +} diff --git a/search/scorer/scorer_constant_test.go b/search/scorer/scorer_constant_test.go new file mode 100644 index 0000000..71f0869 --- /dev/null +++ b/search/scorer/scorer_constant_test.go @@ -0,0 +1,131 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorer + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestConstantScorer(t *testing.T) { + + scorer := NewConstantScorer(1, 1, search.SearcherOptions{Explain: true}) + + tests := []struct { + termMatch *index.TermFieldDoc + result *search.DocumentMatch + }{ + // test some simple math + { + termMatch: &index.TermFieldDoc{ + ID: index.IndexInternalID("one"), + Freq: 1, + Norm: 1.0, + Vectors: []*index.TermFieldVector{ + { + Field: "desc", + Pos: 1, + Start: 0, + End: 4, + }, + }, + }, + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: 1.0, + Expl: &search.Explanation{ + Value: 1.0, + Message: "ConstantScore()", + }, + Sort: []string{}, + }, + }, + } + + for _, test := range tests { + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(1, 0), + } + actual := scorer.Score(ctx, test.termMatch.ID) + + if !reflect.DeepEqual(actual, test.result) { + t.Errorf("expected %#v got %#v for %#v", test.result, actual, test.termMatch) + } + } + +} + +func TestConstantScorerWithQueryNorm(t *testing.T) { + + scorer := NewConstantScorer(1, 1, search.SearcherOptions{Explain: true}) + scorer.SetQueryNorm(2.0) + + tests := []struct { + termMatch *index.TermFieldDoc + result *search.DocumentMatch + }{ + { + termMatch: &index.TermFieldDoc{ + ID: index.IndexInternalID("one"), + Freq: 1, + Norm: 1.0, + }, + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: 2.0, + Sort: []string{}, + Expl: &search.Explanation{ + Value: 2.0, + Message: "weight(^1.000000), product of:", + Children: []*search.Explanation{ + { + Value: 2.0, + Message: "ConstantScore()^1.000000, product of:", + Children: []*search.Explanation{ + { + Value: 1, + Message: "boost", + }, + { + Value: 2, + Message: "queryNorm", + }, + }, + }, + { + Value: 1.0, + Message: "ConstantScore()", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(1, 0), + } + actual := scorer.Score(ctx, test.termMatch.ID) + + if !reflect.DeepEqual(actual, test.result) { + t.Errorf("expected %#v got %#v for %#v", test.result, actual, test.termMatch) + } + } + +} diff --git a/search/scorer/scorer_disjunction.go b/search/scorer/scorer_disjunction.go new file mode 100644 index 0000000..b3e96dd --- /dev/null +++ b/search/scorer/scorer_disjunction.go @@ -0,0 +1,123 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorer + +import ( + "fmt" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" +) + +var reflectStaticSizeDisjunctionQueryScorer int + +func init() { + var dqs DisjunctionQueryScorer + reflectStaticSizeDisjunctionQueryScorer = int(reflect.TypeOf(dqs).Size()) +} + +type DisjunctionQueryScorer struct { + options search.SearcherOptions +} + +func (s *DisjunctionQueryScorer) Size() int { + return reflectStaticSizeDisjunctionQueryScorer + size.SizeOfPtr +} + +func NewDisjunctionQueryScorer(options search.SearcherOptions) *DisjunctionQueryScorer { + return &DisjunctionQueryScorer{ + options: options, + } +} + +func (s *DisjunctionQueryScorer) Score(ctx *search.SearchContext, constituents []*search.DocumentMatch, countMatch, countTotal int) *search.DocumentMatch { + var sum float64 + var childrenExplanations []*search.Explanation + if s.options.Explain { + childrenExplanations = make([]*search.Explanation, len(constituents)) + } + + for i, docMatch := range constituents { + sum += docMatch.Score + if s.options.Explain { + childrenExplanations[i] = docMatch.Expl + } + } + + var rawExpl *search.Explanation + if s.options.Explain { + rawExpl = &search.Explanation{Value: sum, Message: "sum of:", Children: childrenExplanations} + } + + coord := float64(countMatch) / float64(countTotal) + newScore := sum * coord + var newExpl *search.Explanation + if s.options.Explain { + ce := make([]*search.Explanation, 2) + ce[0] = rawExpl + ce[1] = &search.Explanation{Value: coord, Message: fmt.Sprintf("coord(%d/%d)", countMatch, countTotal)} + newExpl = &search.Explanation{Value: newScore, Message: "product of:", Children: ce, PartialMatch: countMatch != countTotal} + } + + // reuse constituents[0] as the return value + rv := constituents[0] + rv.Score = newScore + rv.Expl = newExpl + rv.FieldTermLocations = search.MergeFieldTermLocations( + rv.FieldTermLocations, constituents[1:]) + + return rv +} + +// This method is used only when disjunction searcher is used over multiple +// KNN searchers, where only the score breakdown and the optional explanation breakdown +// is required. The final score and explanation is set when we finalize the KNN hits. +func (s *DisjunctionQueryScorer) ScoreAndExplBreakdown(ctx *search.SearchContext, constituents []*search.DocumentMatch, + matchingIdxs []int, originalPositions []int, countTotal int) *search.DocumentMatch { + + scoreBreakdown := make(map[int]float64) + var childrenExplanations []*search.Explanation + if s.options.Explain { + // since we want to notify which expl belongs to which matched searcher within the disjunction searcher + childrenExplanations = make([]*search.Explanation, countTotal) + } + + for i, docMatch := range constituents { + var index int + if originalPositions != nil { + // scorer used in disjunction slice searcher + index = originalPositions[matchingIdxs[i]] + } else { + // scorer used in disjunction heap searcher + index = matchingIdxs[i] + } + scoreBreakdown[index] = docMatch.Score + if s.options.Explain { + childrenExplanations[index] = docMatch.Expl + } + } + var explBreakdown *search.Explanation + if s.options.Explain { + explBreakdown = &search.Explanation{Children: childrenExplanations} + } + + rv := constituents[0] + rv.ScoreBreakdown = scoreBreakdown + rv.Expl = explBreakdown + rv.FieldTermLocations = search.MergeFieldTermLocations( + rv.FieldTermLocations, constituents[1:]) + return rv +} diff --git a/search/scorer/scorer_knn.go b/search/scorer/scorer_knn.go new file mode 100644 index 0000000..8d90434 --- /dev/null +++ b/search/scorer/scorer_knn.go @@ -0,0 +1,157 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package scorer + +import ( + "fmt" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeKNNQueryScorer int + +func init() { + var sqs KNNQueryScorer + reflectStaticSizeKNNQueryScorer = int(reflect.TypeOf(sqs).Size()) +} + +type KNNQueryScorer struct { + queryVector []float32 + queryField string + queryWeight float64 + queryBoost float64 + queryNorm float64 + options search.SearcherOptions + similarityMetric string + queryWeightExplanation *search.Explanation +} + +func (s *KNNQueryScorer) Size() int { + sizeInBytes := reflectStaticSizeKNNQueryScorer + size.SizeOfPtr + + (len(s.queryVector) * size.SizeOfFloat32) + len(s.queryField) + + len(s.similarityMetric) + + if s.queryWeightExplanation != nil { + sizeInBytes += s.queryWeightExplanation.Size() + } + + return sizeInBytes +} + +func NewKNNQueryScorer(queryVector []float32, queryField string, queryBoost float64, + options search.SearcherOptions, + similarityMetric string) *KNNQueryScorer { + return &KNNQueryScorer{ + queryVector: queryVector, + queryField: queryField, + queryBoost: queryBoost, + queryWeight: 1.0, + options: options, + similarityMetric: similarityMetric, + } +} + +// Score used when the knnMatch.Score = 0 -> +// the query and indexed vector are exactly the same. +const maxKNNScore = math.MaxFloat32 + +func (sqs *KNNQueryScorer) Score(ctx *search.SearchContext, + knnMatch *index.VectorDoc) *search.DocumentMatch { + rv := ctx.DocumentMatchPool.Get() + var scoreExplanation *search.Explanation + score := knnMatch.Score + if sqs.similarityMetric == index.EuclideanDistance { + // in case of euclidean distance being the distance metric, + // an exact vector (perfect match), would return distance = 0 + if score == 0 { + score = maxKNNScore + } else { + // euclidean distances need to be inverted to work with + // tf-idf scoring + score = 1.0 / score + } + } + if sqs.options.Explain { + scoreExplanation = &search.Explanation{ + Value: score, + Message: fmt.Sprintf("fieldWeight(%s in doc %s), score of:", + sqs.queryField, knnMatch.ID), + Children: []*search.Explanation{ + { + Value: score, + Message: fmt.Sprintf("vector(field(%s:%s) with similarity_metric(%s)=%e", + sqs.queryField, knnMatch.ID, sqs.similarityMetric, score), + }, + }, + } + } + // if the query weight isn't 1, multiply + if sqs.queryWeight != 1.0 && score != maxKNNScore { + score = score * sqs.queryWeight + if sqs.options.Explain { + scoreExplanation = &search.Explanation{ + Value: score, + // Product of score * weight + // Avoid adding the query vector to the explanation since vectors + // can get quite large. + Message: fmt.Sprintf("weight(%s:query Vector^%f in %s), product of:", + sqs.queryField, sqs.queryBoost, knnMatch.ID), + Children: []*search.Explanation{sqs.queryWeightExplanation, scoreExplanation}, + } + } + } + rv.Score = score + if sqs.options.Explain { + rv.Expl = scoreExplanation + } + rv.IndexInternalID = append(rv.IndexInternalID, knnMatch.ID...) + return rv +} + +func (sqs *KNNQueryScorer) Weight() float64 { + return 1.0 +} + +func (sqs *KNNQueryScorer) SetQueryNorm(qnorm float64) { + sqs.queryNorm = qnorm + + // update the query weight + sqs.queryWeight = sqs.queryBoost * sqs.queryNorm + + if sqs.options.Explain { + childrenExplanations := make([]*search.Explanation, 2) + childrenExplanations[0] = &search.Explanation{ + Value: sqs.queryBoost, + Message: "boost", + } + childrenExplanations[1] = &search.Explanation{ + Value: sqs.queryNorm, + Message: "queryNorm", + } + sqs.queryWeightExplanation = &search.Explanation{ + Value: sqs.queryWeight, + Message: fmt.Sprintf("queryWeight(%s:query Vector^%f), product of:", + sqs.queryField, sqs.queryBoost), + Children: childrenExplanations, + } + } +} diff --git a/search/scorer/scorer_knn_test.go b/search/scorer/scorer_knn_test.go new file mode 100644 index 0000000..d04edc3 --- /dev/null +++ b/search/scorer/scorer_knn_test.go @@ -0,0 +1,181 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package scorer + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestKNNScorerExplanation(t *testing.T) { + var queryVector []float32 + // arbitrary vector of dims: 64 + for i := 0; i < 64; i++ { + queryVector = append(queryVector, float32(i)) + } + + var resVector []float32 + // arbitrary res vector. + for i := 0; i < 64; i++ { + resVector = append(resVector, float32(i)) + } + + tests := []struct { + vectorMatch *index.VectorDoc + scorer *KNNQueryScorer + norm float64 + result *search.DocumentMatch + }{ + { + vectorMatch: &index.VectorDoc{ + ID: index.IndexInternalID("one"), + Score: 0.5, + Vector: resVector, + }, + norm: 1.0, + scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, + search.SearcherOptions{Explain: true}, index.EuclideanDistance), + // Specifically testing EuclideanDistance since that involves score inversion. + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: 0.5, + Expl: &search.Explanation{ + Value: 1 / 0.5, + Message: "fieldWeight(desc in doc one), score of:", + Children: []*search.Explanation{ + { + Value: 1 / 0.5, + Message: "vector(field(desc:one) with similarity_metric(l2_norm)=2.000000e+00", + }, + }, + }, + }, + }, + { + vectorMatch: &index.VectorDoc{ + ID: index.IndexInternalID("one"), + Score: 0.0, + // Result vector is an exact match of an existing vector. + Vector: queryVector, + }, + norm: 1.0, + scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, + search.SearcherOptions{Explain: true}, index.EuclideanDistance), + // Specifically testing EuclideanDistance with 0 score. + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: 0.0, + Expl: &search.Explanation{ + Value: maxKNNScore, + Message: "fieldWeight(desc in doc one), score of:", + Children: []*search.Explanation{ + { + Value: maxKNNScore, + Message: "vector(field(desc:one) with similarity_metric(l2_norm)=3.402823e+38", + }, + }, + }, + }, + }, + { + vectorMatch: &index.VectorDoc{ + ID: index.IndexInternalID("one"), + Score: 0.5, + Vector: resVector, + }, + norm: 1.0, + scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, + search.SearcherOptions{Explain: true}, index.InnerProduct), + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: 0.5, + Expl: &search.Explanation{ + Value: 0.5, + Message: "fieldWeight(desc in doc one), score of:", + Children: []*search.Explanation{ + { + Value: 0.5, + Message: "vector(field(desc:one) with similarity_metric(dot_product)=5.000000e-01", + }, + }, + }, + }, + }, + { + vectorMatch: &index.VectorDoc{ + ID: index.IndexInternalID("one"), + Score: 0.25, + Vector: resVector, + }, + norm: 0.5, + scorer: NewKNNQueryScorer(queryVector, "desc", 1.0, + search.SearcherOptions{Explain: true}, index.InnerProduct), + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: 0.25, + Expl: &search.Explanation{ + Value: 0.125, + Message: "weight(desc:query Vector^1.000000 in one), product of:", + Children: []*search.Explanation{ + { + Value: 0.5, + Message: "queryWeight(desc:query Vector^1.000000), product of:", + Children: []*search.Explanation{ + { + Value: 1, + Message: "boost", + }, + { + Value: 0.5, + Message: "queryNorm", + }, + }, + }, + { + Value: 0.25, + Message: "fieldWeight(desc in doc one), score of:", + Children: []*search.Explanation{ + { + Value: 0.25, + Message: "vector(field(desc:one) with similarity_metric(dot_product)=2.500000e-01", + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(1, 0), + } + test.scorer.SetQueryNorm(test.norm) + actual := test.scorer.Score(ctx, test.vectorMatch) + actual.Complete(nil) + + if !reflect.DeepEqual(actual.Expl, test.result.Expl) { + t.Errorf("expected %#v got %#v for %#v", test.result.Expl, + actual.Expl, test.vectorMatch) + } + } +} diff --git a/search/scorer/scorer_term.go b/search/scorer/scorer_term.go new file mode 100644 index 0000000..f5f8ec9 --- /dev/null +++ b/search/scorer/scorer_term.go @@ -0,0 +1,276 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorer + +import ( + "fmt" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeTermQueryScorer int + +func init() { + var tqs TermQueryScorer + reflectStaticSizeTermQueryScorer = int(reflect.TypeOf(tqs).Size()) +} + +type TermQueryScorer struct { + queryTerm string + queryField string + queryBoost float64 + docTerm uint64 // number of documents containing the term + docTotal uint64 // total number of documents in the index + avgDocLength float64 + idf float64 + options search.SearcherOptions + idfExplanation *search.Explanation + includeScore bool + queryNorm float64 + queryWeight float64 + queryWeightExplanation *search.Explanation +} + +func (s *TermQueryScorer) Size() int { + sizeInBytes := reflectStaticSizeTermQueryScorer + size.SizeOfPtr + + len(s.queryTerm) + len(s.queryField) + + if s.idfExplanation != nil { + sizeInBytes += s.idfExplanation.Size() + } + + if s.queryWeightExplanation != nil { + sizeInBytes += s.queryWeightExplanation.Size() + } + + return sizeInBytes +} + +func (s *TermQueryScorer) computeIDF(avgDocLength float64, docTotal, docTerm uint64) float64 { + var rv float64 + if avgDocLength > 0 { + // avgDocLength is set only for bm25 scoring + rv = math.Log(1 + (float64(docTotal)-float64(docTerm)+0.5)/ + (float64(docTerm)+0.5)) + } else { + rv = 1.0 + math.Log(float64(docTotal)/ + float64(docTerm+1.0)) + } + + return rv +} + +// queryTerm - the specific term being scored by this scorer object +// queryField - the field in which the term is being searched +// queryBoost - the boost value for the query term +// docTotal - total number of documents in the index +// docTerm - number of documents containing the term +// avgDocLength - average document length in the index +// options - search options such as explain scoring, include the location of the term etc. +func NewTermQueryScorer(queryTerm []byte, queryField string, queryBoost float64, docTotal, + docTerm uint64, avgDocLength float64, options search.SearcherOptions) *TermQueryScorer { + + rv := TermQueryScorer{ + queryTerm: string(queryTerm), + queryField: queryField, + queryBoost: queryBoost, + docTerm: docTerm, + docTotal: docTotal, + avgDocLength: avgDocLength, + options: options, + queryWeight: 1.0, + includeScore: options.Score != "none", + } + + rv.idf = rv.computeIDF(avgDocLength, docTotal, docTerm) + if options.Explain { + rv.idfExplanation = &search.Explanation{ + Value: rv.idf, + Message: fmt.Sprintf("idf(docFreq=%d, maxDocs=%d)", docTerm, docTotal), + } + } + + return &rv +} + +func (s *TermQueryScorer) Weight() float64 { + sum := s.queryBoost * s.idf + return sum * sum +} + +func (s *TermQueryScorer) SetQueryNorm(qnorm float64) { + s.queryNorm = qnorm + + // update the query weight + s.queryWeight = s.queryBoost * s.idf * s.queryNorm + + if s.options.Explain { + childrenExplanations := make([]*search.Explanation, 3) + childrenExplanations[0] = &search.Explanation{ + Value: s.queryBoost, + Message: "boost", + } + childrenExplanations[1] = s.idfExplanation + childrenExplanations[2] = &search.Explanation{ + Value: s.queryNorm, + Message: "queryNorm", + } + s.queryWeightExplanation = &search.Explanation{ + Value: s.queryWeight, + Message: fmt.Sprintf("queryWeight(%s:%s^%f), product of:", s.queryField, s.queryTerm, s.queryBoost), + Children: childrenExplanations, + } + } +} + +func (s *TermQueryScorer) docScore(tf, norm float64) (score float64, model string) { + if s.avgDocLength > 0 { + // bm25 scoring + // using the posting's norm value to recompute the field length for the doc num + fieldLength := 1 / (norm * norm) + + score = s.idf * (tf * search.BM25_k1) / + (tf + search.BM25_k1*(1-search.BM25_b+(search.BM25_b*fieldLength/s.avgDocLength))) + model = index.BM25Scoring + } else { + // tf-idf scoring by default + score = tf * norm * s.idf + model = index.DefaultScoringModel + } + return score, model +} + +func (s *TermQueryScorer) scoreExplanation(tf float64, termMatch *index.TermFieldDoc) []*search.Explanation { + var rv []*search.Explanation + if s.avgDocLength > 0 { + fieldLength := 1 / (termMatch.Norm * termMatch.Norm) + fieldNormVal := 1 - search.BM25_b + (search.BM25_b * fieldLength / s.avgDocLength) + fieldNormalizeExplanation := &search.Explanation{ + Value: fieldNormVal, + Message: fmt.Sprintf("fieldNorm(field=%s), b=%f, fieldLength=%f, avgFieldLength=%f)", + s.queryField, search.BM25_b, fieldLength, s.avgDocLength), + } + + saturationExplanation := &search.Explanation{ + Value: search.BM25_k1 / (tf + search.BM25_k1*fieldNormVal), + Message: fmt.Sprintf("saturation(term:%s), k1=%f/(tf=%f + k1*fieldNorm=%f))", + termMatch.Term, search.BM25_k1, tf, fieldNormVal), + Children: []*search.Explanation{fieldNormalizeExplanation}, + } + + rv = make([]*search.Explanation, 3) + rv[0] = &search.Explanation{ + Value: tf, + Message: fmt.Sprintf("tf(termFreq(%s:%s)=%d", s.queryField, s.queryTerm, termMatch.Freq), + } + rv[1] = saturationExplanation + rv[2] = s.idfExplanation + } else { + rv = make([]*search.Explanation, 3) + rv[0] = &search.Explanation{ + Value: tf, + Message: fmt.Sprintf("tf(termFreq(%s:%s)=%d", s.queryField, s.queryTerm, termMatch.Freq), + } + rv[1] = &search.Explanation{ + Value: termMatch.Norm, + Message: fmt.Sprintf("fieldNorm(field=%s, doc=%s)", s.queryField, termMatch.ID), + } + rv[2] = s.idfExplanation + } + return rv +} + +func (s *TermQueryScorer) Score(ctx *search.SearchContext, termMatch *index.TermFieldDoc) *search.DocumentMatch { + rv := ctx.DocumentMatchPool.Get() + // perform any score computations only when needed + if s.includeScore || s.options.Explain { + var scoreExplanation *search.Explanation + var tf float64 + if termMatch.Freq < MaxSqrtCache { + tf = SqrtCache[int(termMatch.Freq)] + } else { + tf = math.Sqrt(float64(termMatch.Freq)) + } + + score, scoringModel := s.docScore(tf, termMatch.Norm) + if s.options.Explain { + childrenExplanations := s.scoreExplanation(tf, termMatch) + scoreExplanation = &search.Explanation{ + Value: score, + Message: fmt.Sprintf("fieldWeight(%s:%s in %s), as per %s model, "+ + "product of:", s.queryField, s.queryTerm, termMatch.ID, scoringModel), + Children: childrenExplanations, + } + } + + // if the query weight isn't 1, multiply + if s.queryWeight != 1.0 { + score = score * s.queryWeight + if s.options.Explain { + childExplanations := make([]*search.Explanation, 2) + childExplanations[0] = s.queryWeightExplanation + childExplanations[1] = scoreExplanation + scoreExplanation = &search.Explanation{ + Value: score, + Message: fmt.Sprintf("weight(%s:%s^%f in %s), product of:", s.queryField, s.queryTerm, s.queryBoost, termMatch.ID), + Children: childExplanations, + } + } + } + + if s.includeScore { + rv.Score = score + } + + if s.options.Explain { + rv.Expl = scoreExplanation + } + } + + rv.IndexInternalID = append(rv.IndexInternalID, termMatch.ID...) + + if len(termMatch.Vectors) > 0 { + if cap(rv.FieldTermLocations) < len(termMatch.Vectors) { + rv.FieldTermLocations = make([]search.FieldTermLocation, 0, len(termMatch.Vectors)) + } + + for _, v := range termMatch.Vectors { + var ap search.ArrayPositions + if len(v.ArrayPositions) > 0 { + n := len(rv.FieldTermLocations) + if n < cap(rv.FieldTermLocations) { // reuse ap slice if available + ap = rv.FieldTermLocations[:n+1][n].Location.ArrayPositions[:0] + } + ap = append(ap, v.ArrayPositions...) + } + rv.FieldTermLocations = + append(rv.FieldTermLocations, search.FieldTermLocation{ + Field: v.Field, + Term: s.queryTerm, + Location: search.Location{ + Pos: v.Pos, + Start: v.Start, + End: v.End, + ArrayPositions: ap, + }, + }) + } + } + return rv +} diff --git a/search/scorer/scorer_term_test.go b/search/scorer/scorer_term_test.go new file mode 100644 index 0000000..097dbe2 --- /dev/null +++ b/search/scorer/scorer_term_test.go @@ -0,0 +1,260 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorer + +import ( + "math" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestTermScorer(t *testing.T) { + + var docTotal uint64 = 100 + var docTerm uint64 = 9 + var queryTerm = []byte("beer") + var queryField = "desc" + var queryBoost = 1.0 + scorer := NewTermQueryScorer(queryTerm, queryField, queryBoost, docTotal, docTerm, 0, search.SearcherOptions{Explain: true}) + idf := 1.0 + math.Log(float64(docTotal)/float64(docTerm+1.0)) + + tests := []struct { + termMatch *index.TermFieldDoc + result *search.DocumentMatch + }{ + // test some simple math + { + termMatch: &index.TermFieldDoc{ + ID: index.IndexInternalID("one"), + Freq: 1, + Norm: 1.0, + Vectors: []*index.TermFieldVector{ + { + Field: "desc", + Pos: 1, + Start: 0, + End: 4, + }, + }, + }, + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: math.Sqrt(1.0) * idf, + Sort: []string{}, + Expl: &search.Explanation{ + Value: math.Sqrt(1.0) * idf, + Message: "fieldWeight(desc:beer in one), as per tfidf model, product of:", + Children: []*search.Explanation{ + { + Value: 1, + Message: "tf(termFreq(desc:beer)=1", + }, + { + Value: 1, + Message: "fieldNorm(field=desc, doc=one)", + }, + { + Value: idf, + Message: "idf(docFreq=9, maxDocs=100)", + }, + }, + }, + Locations: search.FieldTermLocationMap{ + "desc": search.TermLocationMap{ + "beer": []*search.Location{ + { + Pos: 1, + Start: 0, + End: 4, + }, + }, + }, + }, + }, + }, + // test the same thing again (score should be cached this time) + { + termMatch: &index.TermFieldDoc{ + ID: index.IndexInternalID("one"), + Freq: 1, + Norm: 1.0, + }, + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: math.Sqrt(1.0) * idf, + Sort: []string{}, + Expl: &search.Explanation{ + Value: math.Sqrt(1.0) * idf, + Message: "fieldWeight(desc:beer in one), as per tfidf model, product of:", + Children: []*search.Explanation{ + { + Value: 1, + Message: "tf(termFreq(desc:beer)=1", + }, + { + Value: 1, + Message: "fieldNorm(field=desc, doc=one)", + }, + { + Value: idf, + Message: "idf(docFreq=9, maxDocs=100)", + }, + }, + }, + }, + }, + // test a case where the sqrt isn't precalculated + { + termMatch: &index.TermFieldDoc{ + ID: index.IndexInternalID("one"), + Freq: 65, + Norm: 1.0, + }, + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: math.Sqrt(65) * idf, + Sort: []string{}, + Expl: &search.Explanation{ + Value: math.Sqrt(65) * idf, + Message: "fieldWeight(desc:beer in one), as per tfidf model, product of:", + Children: []*search.Explanation{ + { + Value: math.Sqrt(65), + Message: "tf(termFreq(desc:beer)=65", + }, + { + Value: 1, + Message: "fieldNorm(field=desc, doc=one)", + }, + { + Value: idf, + Message: "idf(docFreq=9, maxDocs=100)", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(1, 0), + } + actual := scorer.Score(ctx, test.termMatch) + actual.Complete(nil) + if len(actual.FieldTermLocations) == 0 { + actual.FieldTermLocations = nil + } + + if !reflect.DeepEqual(actual, test.result) { + t.Errorf("expected %#v got %#v for %#v", test.result, actual, test.termMatch) + } + } + +} + +func TestTermScorerWithQueryNorm(t *testing.T) { + + var docTotal uint64 = 100 + var docTerm uint64 = 9 + var queryTerm = []byte("beer") + var queryField = "desc" + var queryBoost = 3.0 + scorer := NewTermQueryScorer(queryTerm, queryField, queryBoost, docTotal, docTerm, 0, search.SearcherOptions{Explain: true}) + idf := 1.0 + math.Log(float64(docTotal)/float64(docTerm+1.0)) + + scorer.SetQueryNorm(2.0) + + expectedQueryWeight := 3 * idf * 3 * idf + actualQueryWeight := scorer.Weight() + if expectedQueryWeight != actualQueryWeight { + t.Errorf("expected query weight %f, got %f", expectedQueryWeight, actualQueryWeight) + } + + tests := []struct { + termMatch *index.TermFieldDoc + result *search.DocumentMatch + }{ + { + termMatch: &index.TermFieldDoc{ + ID: index.IndexInternalID("one"), + Freq: 1, + Norm: 1.0, + }, + result: &search.DocumentMatch{ + IndexInternalID: index.IndexInternalID("one"), + Score: math.Sqrt(1.0) * idf * 3.0 * idf * 2.0, + Sort: []string{}, + Expl: &search.Explanation{ + Value: math.Sqrt(1.0) * idf * 3.0 * idf * 2.0, + Message: "weight(desc:beer^3.000000 in one), product of:", + Children: []*search.Explanation{ + { + Value: 2.0 * idf * 3.0, + Message: "queryWeight(desc:beer^3.000000), product of:", + Children: []*search.Explanation{ + { + Value: 3, + Message: "boost", + }, + { + Value: idf, + Message: "idf(docFreq=9, maxDocs=100)", + }, + { + Value: 2, + Message: "queryNorm", + }, + }, + }, + { + Value: math.Sqrt(1.0) * idf, + Message: "fieldWeight(desc:beer in one), as per tfidf model, product of:", + Children: []*search.Explanation{ + { + Value: 1, + Message: "tf(termFreq(desc:beer)=1", + }, + { + Value: 1, + Message: "fieldNorm(field=desc, doc=one)", + }, + { + Value: idf, + Message: "idf(docFreq=9, maxDocs=100)", + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(1, 0), + } + actual := scorer.Score(ctx, test.termMatch) + + if !reflect.DeepEqual(actual, test.result) { + t.Errorf("expected %#v got %#v for %#v", test.result, actual, test.termMatch) + } + } + +} diff --git a/search/scorer/sqrt_cache.go b/search/scorer/sqrt_cache.go new file mode 100644 index 0000000..e26d33d --- /dev/null +++ b/search/scorer/sqrt_cache.go @@ -0,0 +1,30 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorer + +import ( + "math" +) + +var SqrtCache []float64 + +const MaxSqrtCache = 64 + +func init() { + SqrtCache = make([]float64, MaxSqrtCache) + for i := 0; i < MaxSqrtCache; i++ { + SqrtCache[i] = math.Sqrt(float64(i)) + } +} diff --git a/search/search.go b/search/search.go new file mode 100644 index 0000000..5c930bc --- /dev/null +++ b/search/search.go @@ -0,0 +1,396 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "fmt" + "reflect" + "sort" + + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var ( + reflectStaticSizeDocumentMatch int + reflectStaticSizeSearchContext int + reflectStaticSizeLocation int +) + +func init() { + var dm DocumentMatch + reflectStaticSizeDocumentMatch = int(reflect.TypeOf(dm).Size()) + var sc SearchContext + reflectStaticSizeSearchContext = int(reflect.TypeOf(sc).Size()) + var l Location + reflectStaticSizeLocation = int(reflect.TypeOf(l).Size()) +} + +type ArrayPositions []uint64 + +func (ap ArrayPositions) Equals(other ArrayPositions) bool { + if len(ap) != len(other) { + return false + } + for i := range ap { + if ap[i] != other[i] { + return false + } + } + return true +} + +func (ap ArrayPositions) Compare(other ArrayPositions) int { + for i, p := range ap { + if i >= len(other) { + return 1 + } + if p < other[i] { + return -1 + } + if p > other[i] { + return 1 + } + } + if len(ap) < len(other) { + return -1 + } + return 0 +} + +type Location struct { + // Pos is the position of the term within the field, starting at 1 + Pos uint64 `json:"pos"` + + // Start and End are the byte offsets of the term in the field + Start uint64 `json:"start"` + End uint64 `json:"end"` + + // ArrayPositions contains the positions of the term within any elements. + ArrayPositions ArrayPositions `json:"array_positions"` +} + +func (l *Location) Size() int { + return reflectStaticSizeLocation + size.SizeOfPtr + + len(l.ArrayPositions)*size.SizeOfUint64 +} + +type Locations []*Location + +func (p Locations) Len() int { return len(p) } +func (p Locations) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +func (p Locations) Less(i, j int) bool { + c := p[i].ArrayPositions.Compare(p[j].ArrayPositions) + if c < 0 { + return true + } + if c > 0 { + return false + } + return p[i].Pos < p[j].Pos +} + +func (p Locations) Dedupe() Locations { // destructive! + if len(p) <= 1 { + return p + } + + sort.Sort(p) + + slow := 0 + + for _, pfast := range p { + pslow := p[slow] + if pslow.Pos == pfast.Pos && + pslow.Start == pfast.Start && + pslow.End == pfast.End && + pslow.ArrayPositions.Equals(pfast.ArrayPositions) { + continue // duplicate, so only move fast ahead + } + + slow++ + + p[slow] = pfast + } + + return p[:slow+1] +} + +type TermLocationMap map[string]Locations + +func (t TermLocationMap) AddLocation(term string, location *Location) { + t[term] = append(t[term], location) +} + +type FieldTermLocationMap map[string]TermLocationMap + +type FieldTermLocation struct { + Field string + Term string + Location Location +} + +type FieldFragmentMap map[string][]string + +type DocumentMatch struct { + Index string `json:"index,omitempty"` + ID string `json:"id"` + IndexInternalID index.IndexInternalID `json:"-"` + Score float64 `json:"score"` + Expl *Explanation `json:"explanation,omitempty"` + Locations FieldTermLocationMap `json:"locations,omitempty"` + Fragments FieldFragmentMap `json:"fragments,omitempty"` + Sort []string `json:"sort,omitempty"` + + // Fields contains the values for document fields listed in + // SearchRequest.Fields. Text fields are returned as strings, numeric + // fields as float64s and date fields as strings. + Fields map[string]interface{} `json:"fields,omitempty"` + + // used to maintain natural index order + HitNumber uint64 `json:"-"` + + // used to temporarily hold field term location information during + // search processing in an efficient, recycle-friendly manner, to + // be later incorporated into the Locations map when search + // results are completed + FieldTermLocations []FieldTermLocation `json:"-"` + + // used to indicate the sub-scores that combined to form the + // final score for this document match. This is only populated + // when the search request's query is a DisjunctionQuery + // or a ConjunctionQuery. The map key is the index of the sub-query + // in the DisjunctionQuery or ConjunctionQuery. The map value is the + // sub-score for that sub-query. + ScoreBreakdown map[int]float64 `json:"score_breakdown,omitempty"` + + // internal variable used in PreSearch phase of search in alias + // to indicate the name of the index that this match came from. + // used in knn search. + // it is a stack of index names, the top of the stack is the name + // of the index that this match came from + // of the current alias view, used in alias of aliases scenario + IndexNames []string `json:"index_names,omitempty"` +} + +func (dm *DocumentMatch) AddFieldValue(name string, value interface{}) { + if dm.Fields == nil { + dm.Fields = make(map[string]interface{}) + } + existingVal, ok := dm.Fields[name] + if !ok { + dm.Fields[name] = value + return + } + + valSlice, ok := existingVal.([]interface{}) + if ok { + // already a slice, append to it + valSlice = append(valSlice, value) + } else { + // create a slice + valSlice = []interface{}{existingVal, value} + } + dm.Fields[name] = valSlice +} + +// Reset allows an already allocated DocumentMatch to be reused +func (dm *DocumentMatch) Reset() *DocumentMatch { + // remember the []byte used for the IndexInternalID + indexInternalID := dm.IndexInternalID + // remember the []interface{} used for sort + sort := dm.Sort + // remember the FieldTermLocations backing array + ftls := dm.FieldTermLocations + for i := range ftls { // recycle the ArrayPositions of each location + ftls[i].Location.ArrayPositions = ftls[i].Location.ArrayPositions[:0] + } + // idiom to copy over from empty DocumentMatch (0 allocations) + *dm = DocumentMatch{} + // reuse the []byte already allocated (and reset len to 0) + dm.IndexInternalID = indexInternalID[:0] + // reuse the []interface{} already allocated (and reset len to 0) + dm.Sort = sort[:0] + // reuse the FieldTermLocations already allocated (and reset len to 0) + dm.FieldTermLocations = ftls[:0] + return dm +} + +func (dm *DocumentMatch) Size() int { + sizeInBytes := reflectStaticSizeDocumentMatch + size.SizeOfPtr + + len(dm.Index) + + len(dm.ID) + + len(dm.IndexInternalID) + + if dm.Expl != nil { + sizeInBytes += dm.Expl.Size() + } + + for k, v := range dm.Locations { + sizeInBytes += size.SizeOfString + len(k) + for k1, v1 := range v { + sizeInBytes += size.SizeOfString + len(k1) + + size.SizeOfSlice + for _, entry := range v1 { + sizeInBytes += entry.Size() + } + } + } + + for k, v := range dm.Fragments { + sizeInBytes += size.SizeOfString + len(k) + + size.SizeOfSlice + + for _, entry := range v { + sizeInBytes += size.SizeOfString + len(entry) + } + } + + for _, entry := range dm.Sort { + sizeInBytes += size.SizeOfString + len(entry) + } + + for k := range dm.Fields { + sizeInBytes += size.SizeOfString + len(k) + + size.SizeOfPtr + } + + return sizeInBytes +} + +// Complete performs final preparation & transformation of the +// DocumentMatch at the end of search processing, also allowing the +// caller to provide an optional preallocated locations slice +func (dm *DocumentMatch) Complete(prealloc []Location) []Location { + // transform the FieldTermLocations slice into the Locations map + nlocs := len(dm.FieldTermLocations) + if nlocs > 0 { + if cap(prealloc) < nlocs { + prealloc = make([]Location, nlocs) + } + prealloc = prealloc[:nlocs] + + var lastField string + var tlm TermLocationMap + var needsDedupe bool + + for i, ftl := range dm.FieldTermLocations { + if i == 0 || lastField != ftl.Field { + lastField = ftl.Field + + if dm.Locations == nil { + dm.Locations = make(FieldTermLocationMap) + } + + tlm = dm.Locations[ftl.Field] + if tlm == nil { + tlm = make(TermLocationMap) + dm.Locations[ftl.Field] = tlm + } + } + + loc := &prealloc[i] + *loc = ftl.Location + + if len(loc.ArrayPositions) > 0 { // copy + loc.ArrayPositions = append(ArrayPositions(nil), loc.ArrayPositions...) + } + + locs := tlm[ftl.Term] + + // if the loc is before or at the last location, then there + // might be duplicates that need to be deduplicated + if !needsDedupe && len(locs) > 0 { + last := locs[len(locs)-1] + cmp := loc.ArrayPositions.Compare(last.ArrayPositions) + if cmp < 0 || (cmp == 0 && loc.Pos <= last.Pos) { + needsDedupe = true + } + } + + tlm[ftl.Term] = append(locs, loc) + + dm.FieldTermLocations[i] = FieldTermLocation{ // recycle + Location: Location{ + ArrayPositions: ftl.Location.ArrayPositions[:0], + }, + } + } + + if needsDedupe { + for _, tlm := range dm.Locations { + for term, locs := range tlm { + tlm[term] = locs.Dedupe() + } + } + } + } + + dm.FieldTermLocations = dm.FieldTermLocations[:0] // recycle + + return prealloc +} + +func (dm *DocumentMatch) String() string { + return fmt.Sprintf("[%s-%f]", dm.ID, dm.Score) +} + +type DocumentMatchCollection []*DocumentMatch + +func (c DocumentMatchCollection) Len() int { return len(c) } +func (c DocumentMatchCollection) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c DocumentMatchCollection) Less(i, j int) bool { return c[i].Score > c[j].Score } + +type Searcher interface { + Next(ctx *SearchContext) (*DocumentMatch, error) + Advance(ctx *SearchContext, ID index.IndexInternalID) (*DocumentMatch, error) + Close() error + Weight() float64 + SetQueryNorm(float64) + Count() uint64 + Min() int + Size() int + + DocumentMatchPoolSize() int +} + +type SearcherOptions struct { + Explain bool + IncludeTermVectors bool + Score string +} + +// SearchContext represents the context around a single search +type SearchContext struct { + DocumentMatchPool *DocumentMatchPool + Collector Collector + IndexReader index.IndexReader +} + +func (sc *SearchContext) Size() int { + sizeInBytes := reflectStaticSizeSearchContext + size.SizeOfPtr + + reflectStaticSizeDocumentMatchPool + size.SizeOfPtr + + if sc.DocumentMatchPool != nil { + for _, entry := range sc.DocumentMatchPool.avail { + if entry != nil { + sizeInBytes += entry.Size() + } + } + } + + return sizeInBytes +} diff --git a/search/search_test.go b/search/search_test.go new file mode 100644 index 0000000..af81391 --- /dev/null +++ b/search/search_test.go @@ -0,0 +1,94 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestArrayPositionsCompare(t *testing.T) { + tests := []struct { + a []uint64 + b []uint64 + expect int + }{ + {nil, nil, 0}, + {[]uint64{}, []uint64{}, 0}, + {[]uint64{1}, []uint64{}, 1}, + {[]uint64{1}, []uint64{1}, 0}, + {[]uint64{}, []uint64{1}, -1}, + {[]uint64{0}, []uint64{1}, -1}, + {[]uint64{1}, []uint64{0}, 1}, + {[]uint64{1}, []uint64{1, 2}, -1}, + {[]uint64{1, 2}, []uint64{1}, 1}, + {[]uint64{1, 2}, []uint64{1, 2}, 0}, + {[]uint64{1, 2}, []uint64{1, 200}, -1}, + {[]uint64{1, 2}, []uint64{100, 2}, -1}, + {[]uint64{1, 2}, []uint64{1, 2, 3}, -1}, + } + + for _, test := range tests { + res := ArrayPositions(test.a).Compare(test.b) + if res != test.expect { + t.Errorf("test: %+v, res: %v", test, res) + } + } +} + +func TestLocationsDedupe(t *testing.T) { + a := &Location{} + b := &Location{Pos: 1} + c := &Location{Pos: 2} + + tests := []struct { + input Locations + expect Locations + }{ + {Locations{}, Locations{}}, + {Locations{a}, Locations{a}}, + {Locations{a, b, c}, Locations{a, b, c}}, + {Locations{a, a}, Locations{a}}, + {Locations{a, a, a}, Locations{a}}, + {Locations{a, b}, Locations{a, b}}, + {Locations{b, a}, Locations{a, b}}, + {Locations{c, b, a, c, b, a, c, b, a}, Locations{a, b, c}}, + } + + for testi, test := range tests { + res := test.input.Dedupe() + if !reflect.DeepEqual(res, test.expect) { + t.Errorf("testi: %d, test: %+v, res: %+v", testi, test, res) + } + } +} + +func TestMarshallingHighTerm(t *testing.T) { + highTermBytes, err := json.Marshal(HighTerm) + if err != nil { + t.Fatal(err) + } + + var unmarshalledHighTerm string + err = json.Unmarshal(highTermBytes, &unmarshalledHighTerm) + if err != nil { + t.Fatal(err) + } + + if unmarshalledHighTerm != HighTerm { + t.Fatalf("unexpected %x != %x", unmarshalledHighTerm, HighTerm) + } +} diff --git a/search/searcher/base_test.go b/search/searcher/base_test.go new file mode 100644 index 0000000..6f80bf6 --- /dev/null +++ b/search/searcher/base_test.go @@ -0,0 +1,117 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "math" + "regexp" + + "github.com/blevesearch/bleve/v2/analysis" + regexpTokenizer "github.com/blevesearch/bleve/v2/analysis/tokenizer/regexp" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + index "github.com/blevesearch/bleve_index_api" +) + +var twoDocIndex index.Index + +func init() { + twoDocIndex = initTwoDocUpsideDown() +} + +func initTwoDocUpsideDown() index.Index { + analysisQueue := index.NewAnalysisQueue(1) + twoDocIndex, err := upsidedown.NewUpsideDownCouch( + gtreap.Name, + map[string]interface{}{ + "path": "", + }, analysisQueue) + if err != nil { + panic(err) + } + initTwoDocs(twoDocIndex) + return twoDocIndex +} + +func initTwoDocScorch(dir string) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + twoDocIndex, err := scorch.NewScorch( + scorch.Name, + map[string]interface{}{ + "path": dir, + }, analysisQueue) + if err != nil { + panic(err) + } + initTwoDocs(twoDocIndex) + return twoDocIndex +} + +func initTwoDocs(twoDocIndex index.Index) { + err := twoDocIndex.Open() + if err != nil { + panic(err) + } + batch := index.NewBatch() + for _, doc := range twoDocIndexDocs { + batch.Update(doc) + } + err = twoDocIndex.Batch(batch) + if err != nil { + panic(err) + } +} + +// create a simpler analyzer which will support these tests +var testAnalyzer = &analysis.DefaultAnalyzer{ + Tokenizer: regexpTokenizer.NewRegexpTokenizer(regexp.MustCompile(`\w+`)), +} + +// sets up some mock data used in many tests in this package +var twoDocIndexDescIndexingOptions = document.DefaultTextIndexingOptions | index.IncludeTermVectors + +var twoDocIndexDocs = []*document.Document{ + // must have 4/4 beer + document.NewDocument("1"). + AddField(document.NewTextField("name", []uint64{}, []byte("marty"))). + AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("beer beer beer beer"), twoDocIndexDescIndexingOptions, testAnalyzer)). + AddField(document.NewTextFieldWithAnalyzer("street", []uint64{}, []byte("couchbase way"), testAnalyzer)), + // must have 1/4 beer + document.NewDocument("2"). + AddField(document.NewTextField("name", []uint64{}, []byte("steve"))). + AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("angst beer couch database"), twoDocIndexDescIndexingOptions, testAnalyzer)). + AddField(document.NewTextFieldWithAnalyzer("street", []uint64{}, []byte("couchbase way"), testAnalyzer)). + AddField(document.NewTextFieldWithAnalyzer("title", []uint64{}, []byte("mister"), testAnalyzer)), + // must have 1/4 beer + document.NewDocument("3"). + AddField(document.NewTextField("name", []uint64{}, []byte("dustin"))). + AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("apple beer column dank"), twoDocIndexDescIndexingOptions, testAnalyzer)). + AddField(document.NewTextFieldWithAnalyzer("title", []uint64{}, []byte("mister"), testAnalyzer)), + // must have 65/65 beer + document.NewDocument("4"). + AddField(document.NewTextField("name", []uint64{}, []byte("ravi"))). + AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer beer"), twoDocIndexDescIndexingOptions, testAnalyzer)), + // must have 0/x beer + document.NewDocument("5"). + AddField(document.NewTextField("name", []uint64{}, []byte("bobert"))). + AddField(document.NewTextFieldCustom("desc", []uint64{}, []byte("water"), twoDocIndexDescIndexingOptions, testAnalyzer)). + AddField(document.NewTextFieldWithAnalyzer("title", []uint64{}, []byte("mister"), testAnalyzer)), +} + +func scoresCloseEnough(a, b float64) bool { + return math.Abs(a-b) < 0.001 +} diff --git a/search/searcher/geoshape_contains_test.go b/search/searcher/geoshape_contains_test.go new file mode 100644 index 0000000..437091a --- /dev/null +++ b/search/searcher/geoshape_contains_test.go @@ -0,0 +1,1006 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +var ( + leftRectEdgeMultiPoint [][]float64 = [][]float64{{-1, 0.2}, {-0.9, 0.1}} + leftRectWithHole [][][]float64 = [][][]float64{ + {{-1, 0}, {0, 0}, {0, 1}, {-1, 1}, {-1, 0}}, + {{-0.75, 0.25}, {-0.75, -0.75}, {-0.25, 0.75}, {-0.25, 0.25}, {-0.74, 0.25}}, + } + leftRectEdgePoint []float64 = []float64{-1, 0.2} + leftRectMultiPoint [][]float64 = [][]float64{{0.5, 0.5}, {-0.9, 0.1}} +) + +func testCaseSetup(t *testing.T, docShapeName, docShapeType string, docShapeVertices [][][][]float64, + i index.Index, +) (index.IndexReader, func() error, error) { + doc := document.NewDocument(docShapeName) + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + docShapeVertices, docShapeType, document.DefaultGeoShapeIndexingOptions)) + err := i.Update(doc) + if err != nil { + return nil, nil, err + } + indexReader, err := i.Reader() + if err != nil { + t.Fatal(err) + } + + closeFn := func() error { + err = i.Delete(doc.ID()) + if err != nil { + return err + } + err = indexReader.Close() + if err != nil { + return err + } + return nil + } + + return indexReader, closeFn, nil +} + +func TestPointPolygonContains(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: rightRectPoint, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "point inside polygon", + QueryType: "contains", + }, + { + QueryShape: leftRectPoint, + DocShapeVertices: nil, + DocShapeName: "", + Expected: nil, + Desc: "empty polygon", + QueryType: "contains", + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestLinestringPolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{1, 2}, {3, 5}}, + DocShapeVertices: [][][]float64{{{1, 2}, {3, 5}, {2, 7}, {1, 2}}}, + DocShapeName: "polygon1", + Desc: "linestring coinciding with edge of the polygon", + Expected: []string{"polygon1"}, + QueryType: "contains", + }, + { + QueryShape: [][]float64{{1, 0}, {0, 1}}, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Desc: "diagonal of a square", + Expected: []string{"polygon1"}, + QueryType: "contains", + }, + { + QueryShape: [][]float64{{0.2, 0.2}, {0.8, 0.8}}, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Desc: "linestring within polygon", + Expected: []string{"polygon1"}, + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeLinestringQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for linestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopePointContains(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices []float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: []float64{0.5, 0.5}, + DocShapeName: "point1", + Desc: "point completely within bounded rectangle", + Expected: nil, // will always be nil since point can't contain envelope + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopeLinestringContains(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][]float64{{0.5, 0.5}, {10, 10}}, + DocShapeName: "linestring1", + Desc: "linestring partially within bounded rectangle", + Expected: nil, // will always be nil since linestring can't contain envelope + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopePolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][][]float64{{{0.5, 0.5}, {1, 0.5}, {1, 1}, {0.5, 1}, {0.5, 0.5}}}, + DocShapeName: "polygon1", + Desc: "polygon completely within bounded rectangle", + Expected: nil, + QueryType: "contains", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][][]float64{{{10.5, 10.5}, {11.5, 10.5}, {11.5, 11.5}, {10.5, 11.5}, {10.5, 10.5}}}, + DocShapeName: "polygon1", + Desc: "polygon completely outside bounded rectangle", + Expected: nil, + QueryType: "contains", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Desc: "polygon coincident with bounded rectangle", + Expected: []string{"polygon1"}, + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonPointContains(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices []float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: rightRect, + DocShapeVertices: rightRectPoint, + DocShapeName: "point1", + Expected: nil, // nil since point is a non-closed shape + Desc: "point inside polygon", + QueryType: "contains", + }, + { + QueryShape: leftRect, + DocShapeVertices: leftRectEdgePoint, + DocShapeName: "point1", + Expected: nil, + Desc: "point on edge of polygon", + QueryType: "contains", + }, + { + QueryShape: leftRectWithHole, + DocShapeVertices: leftRectPoint, + DocShapeName: "point1", + Expected: nil, + Desc: "point in polygon's hole", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonLinestringContains(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: rightRect, + DocShapeVertices: [][]float64{{0, 1}, {1, 0}}, + DocShapeName: "linestring1", + Expected: nil, // nil since linestring is a non-closed shape + Desc: "diagonal of a square", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonEnvelopeContains(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][]float64{{{0.5, 0.5}, {1, 0.5}, {1, 1}, {0.5, 1}, {0.5, 0.5}}}, + DocShapeVertices: [][]float64{{0, 1}, {1, 0}}, + DocShapeName: "envelope1", + Expected: nil, + Desc: "polygon contained inside envelope with edge overlaps", // this fails since + // contains doesn't include edges or vertices + QueryType: "contains", + }, + { + QueryShape: [][][]float64{{{0.25, 0.25}, {0.5, 0.25}, {0.5, 0.5}, {0.25, 0.25}, {0.25, 0.25}}}, + DocShapeVertices: [][]float64{{0, 1}, {1, 0}}, + DocShapeName: "envelope1", + Expected: []string{"envelope1"}, + Desc: "polygon contained completely inside envelope", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "envelope", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPointPolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: leftRectEdgeMultiPoint, + DocShapeVertices: leftRectWithHole, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "multi point inside polygon with hole", + QueryType: "contains", + }, + { + QueryShape: [][]float64{{1, 0.5}}, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Expected: nil, + Desc: "multi point on polygon edge", + QueryType: "contains", + }, + { + QueryShape: [][]float64{{0.3, 0.3}, {0.5, 0.5}}, + DocShapeVertices: [][][]float64{ + {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}, + {{0.2, 0.2}, {0.4, 0.2}, {0.4, 0.4}, {0.2, 0.4}, {0.2, 0.2}}, + }, + DocShapeName: "polygon1", + Expected: nil, // returns nil since one of the points is within the hole + Desc: "multi point inside polygon and hole", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + true, indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipoint: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPointLinestringContains(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: leftRectEdgeMultiPoint, + DocShapeVertices: [][]float64{{-1, 0.2}, {-0.9, 0.1}}, + DocShapeName: "linestring1", + Expected: []string{"linestring1"}, + Desc: "multi point overlaps with all linestring end points", + QueryType: "contains", + }, + { + QueryShape: [][]float64{{-1, 0.2}, {-0.9, 0.1}, {0.5, 0.5}}, + DocShapeVertices: [][]float64{{-1, 0.2}, {-0.9, 0.1}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "multi point overlaps with some linestring end points", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + true, indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipoint: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPointContains(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: leftRectEdgeMultiPoint, + DocShapeVertices: [][]float64{{-1, 0.2}, {-0.9, 0.1}}, + DocShapeName: "multipoint1", + Expected: []string{"multipoint1"}, + Desc: "multi point overlaps with all multi points", + QueryType: "contains", + }, + { + QueryShape: [][]float64{{-1, 0.2}, {-0.9, 0.1}, {0.5, 0.5}}, + DocShapeVertices: [][]float64{{-1, 0.2}, {-0.9, 0.1}}, + DocShapeName: "multipoint1", + Expected: nil, + Desc: "multi point overlaps with some multi points", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipoint", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + true, indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipoint: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: leftRect, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Expected: nil, + Desc: "polygons sharing an edge", + QueryType: "contains", + }, + { + QueryShape: rightRect, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "coincident polygons", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonMultiPointContains(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: leftRect, + DocShapeVertices: leftRectEdgeMultiPoint, + DocShapeName: "multipoint1", + Expected: nil, // nil since multipoint is a non-closed shape + Desc: "multiple points on polygon edge", + QueryType: "contains", + }, + { + QueryShape: leftRect, + DocShapeVertices: leftRectMultiPoint, + DocShapeName: "multipoint1", + Expected: nil, + Desc: "multiple points, both outside and inside polygon", + QueryType: "contains", + }, + { + QueryShape: leftRectWithHole, + DocShapeVertices: leftRectMultiPoint, + DocShapeName: "multipoint1", + Expected: nil, + Desc: "multiple points in polygon hole", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipoint", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPolygonPolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][][]float64{leftRect}, + DocShapeVertices: leftRect, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "coincident polygons", + QueryType: "contains", + }, + { + QueryShape: [][][][]float64{{{{2, 2}, {-2, 2}, {-2, -2}, {2, -2}}}}, + DocShapeVertices: leftRect, + DocShapeName: "polygon1", + Expected: nil, + Desc: "polygon larger than polygons in query shape", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiPolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiLinestringMultiPolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][]float64{{{0.2, 1}, {0.8, 1}}, {{1, 0.2}, {1, 0.8}}}, + DocShapeVertices: [][][][]float64{rightRect}, + DocShapeName: "multipolygon1", + Expected: nil, // contains doesn't include edges or vertices + Desc: "linestrings on edge of polygon", + QueryType: "contains", + }, + { + QueryShape: [][][]float64{{{0.2, 0.2}, {0.8, 0.8}}, {{0.8, 0.2}, {0.2, 0.8}}}, + DocShapeVertices: [][][][]float64{{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}}, + DocShapeName: "multipolygon1", + Expected: []string{"multipolygon1"}, + Desc: "linestrings within polygon", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipolygon", test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiLinestringQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multilinestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionPolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][][][][]float64 + QueryShapeTypes []string + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][][][]float64{{{{{0, 1}, {1, 0}}}}}, + QueryShapeTypes: []string{"linestring"}, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "linestring on edge of polygon", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeGeometryCollectionRelationQuery(test.QueryType, + indexReader, test.QueryShape, test.QueryShapeTypes, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionMultiPolygonContains(t *testing.T) { + tests := []struct { + QueryShape [][][][][]float64 + QueryShapeTypes []string + DocShapeVertices [][][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][][][]float64{{{{{1, 1}}}}}, + QueryShapeTypes: []string{"point"}, + DocShapeVertices: [][][][]float64{rightRect, leftRect}, + DocShapeName: "multipolygon1", + Expected: []string{"multipolygon1"}, + Desc: "point on vertex of one of the polygons", + QueryType: "contains", + }, + { + // WIP - Adding a point (-0.5,-0.5) + QueryShape: [][][][][]float64{{{{{0.2, 0.4}, {0.2, 0.2}, {0.4, 0.2}, {0.4, 0.4}}}}}, + QueryShapeTypes: []string{"polygon"}, + DocShapeVertices: [][][][]float64{rightRect, leftRect}, + DocShapeName: "multipolygon1", + Expected: []string{"multipolygon1"}, + Desc: "polygon contained completely within multipolygons", + QueryType: "contains", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipolygon", test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeGeometryCollectionRelationQuery(test.QueryType, + indexReader, test.QueryShape, test.QueryShapeTypes, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipolygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} diff --git a/search/searcher/geoshape_intersects_test.go b/search/searcher/geoshape_intersects_test.go new file mode 100644 index 0000000..8de2237 --- /dev/null +++ b/search/searcher/geoshape_intersects_test.go @@ -0,0 +1,1785 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + index "github.com/blevesearch/bleve_index_api" +) + +func setupIndex(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + return i +} + +func TestPointIntersects(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices []float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: []float64{2.0, 2.0}, + DocShapeVertices: []float64{2.0, 2.0}, + DocShapeName: "point1", + Desc: "coincident points", + Expected: []string{"point1"}, + }, + { + QueryShape: []float64{2.0, 2.0}, + DocShapeVertices: []float64{2.0, 2.1}, + DocShapeName: "point2", + Desc: "non coincident points", + Expected: nil, + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + // indexing and searching independently for each case. + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointMultiPointIntersects(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: []float64{2.0, 2.0}, + DocShapeVertices: [][]float64{{2.0, 2.0}, {3.0, 2.0}}, + DocShapeName: "point1", + Desc: "point coincides with one point in multipoint", + Expected: []string{"point1"}, + }, + { + QueryShape: []float64{2.0, 2.0}, + DocShapeVertices: [][]float64{{2.0, 2.1}, {3.0, 3.1}}, + DocShapeName: "point2", + Desc: "non coincident points", + Expected: nil, + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + // indexing and searching independently for each case. + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: []float64{4.0, 4.0}, + DocShapeVertices: [][]float64{{2.0, 2.0}, {3.0, 3.0}, {4.0, 4.0}}, + DocShapeName: "linestring1", + Desc: "point at the vertex of linestring", + Expected: []string{"linestring1"}, + }, + { + QueryShape: []float64{1.5, 1.5001714}, + DocShapeVertices: [][]float64{{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeName: "linestring1", + Desc: "point along linestring", + Expected: nil, // nil since point is said to intersect only when it matches any + // of the endpoints of the linestring + }, + { + QueryShape: []float64{1.5, 1.6001714}, + DocShapeVertices: [][]float64{{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeName: "linestring1", + Desc: "point outside linestring", + Expected: nil, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointMultiLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: []float64{3.0, 3.0}, + DocShapeVertices: [][][]float64{{{2.0, 2.0}, {3.0, 3.0}, {4.0, 4.0}}}, + DocShapeName: "linestring1", + Desc: "point at the vertex of linestring", + Expected: []string{"linestring1"}, + }, + { + QueryShape: []float64{1.5, 1.5001714}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}}, + DocShapeName: "linestring1", + Desc: "point along a linestring", + Expected: nil, // nil since point is said to intersect only when it matches any + // of the endpoints of any of the linestrings + }, + { + QueryShape: []float64{1.5, 1.6001714}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, {{1, 1.1}, {2, 2.1}, {3, 3.4}}}, + DocShapeName: "linestring1", + Desc: "point outside all linestrings", + Expected: nil, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multilinestring", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointPolygonIntersects(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: []float64{3.0, 3.0}, + DocShapeVertices: [][][]float64{{{2.0, 2.0}, {3.0, 3.0}, {1.0, 3.0}, {2.0, 2.0}}}, + DocShapeName: "polygon1", + Desc: "point on polygon vertex", + Expected: []string{"polygon1"}, + }, + { + QueryShape: []float64{1.5, 1.500714}, + DocShapeVertices: [][][]float64{{{1.0, 1.0}, {2.0, 2.0}, {0.0, 2.0}, {1.0, 1.0}}}, + DocShapeName: "polygon1", + Desc: "point on polygon edge", + Expected: []string{"polygon1"}, + }, + { + QueryShape: []float64{1.5, 1.9}, + DocShapeVertices: [][][]float64{{{1.0, 1.0}, {2.0, 2.0}, {0.0, 2.0}, {1.0, 1.0}}}, + DocShapeName: "polygon1", + Desc: "point inside polygon", + Expected: []string{"polygon1"}, + }, + { + QueryShape: []float64{0.3, 0.3}, + DocShapeVertices: [][][]float64{ + {{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}, + {{0.2, 0.2}, {0.2, 0.4}, {0.4, 0.4}, {0.4, 0.2}, {0.2, 0.2}}, + }, + DocShapeName: "polygon1", + Desc: "point inside hole inside polygon", + Expected: nil, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointMultiPolygonIntersects(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: []float64{3.0, 3.0}, + DocShapeVertices: [][][][]float64{{{{2.0, 2.0}, {3.0, 3.0}, {1.0, 3.0}, {2.0, 2.0}}}}, + DocShapeName: "multipolygon1", + Desc: "point on a polygon vertex", + Expected: []string{"multipolygon1"}, + }, + { + QueryShape: []float64{1.5, 1.500714}, + DocShapeVertices: [][][][]float64{{{{1.0, 1.0}, {2.0, 2.0}, {0.0, 2.0}, {1.0, 1.0}}}}, + DocShapeName: "multipolygon1", + Desc: "point on polygon edge", + Expected: []string{"multipolygon1"}, + }, + { + QueryShape: []float64{1.5, 1.9}, + DocShapeVertices: [][][][]float64{ + {{{1.0, 1.0}, {2.0, 2.0}, {0.0, 2.0}, {1.0, 1.0}}}, + {{{1.5, 1.9}, {2.5, 2.9}, {0.5, 2.9}, {1.5, 1.9}}}, + }, + DocShapeName: "multipolygon1", + Desc: "point inside a polygon and on vertex of another polygon", + Expected: []string{"multipolygon1"}, + }, + { + QueryShape: []float64{0.3, 0.3}, + DocShapeVertices: [][][][]float64{{ + {{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}, + {{0.2, 0.2}, {0.2, 0.4}, {0.4, 0.4}, {0.4, 0.2}, {0.2, 0.2}}, + }}, + DocShapeName: "multipolygon1", + Desc: "point inside hole inside one of the polygons", + Expected: nil, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipolygon", test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopePointIntersects(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices []float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: rightRectPoint, + DocShapeName: "point1", + Desc: "point on vertex of bounded rectangle", + Expected: []string{"point1"}, + QueryType: "intersects", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: []float64{10, 10}, + DocShapeName: "point1", + Desc: "point outside bounded rectangle", + Expected: nil, + QueryType: "intersects", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopeLinestringIntersect(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][]float64{{0.25, 0.25}, {0.5, 0.5}}, + DocShapeName: "linestring1", + Desc: "linestring completely in bounded rectangle", + Expected: []string{"linestring1"}, + QueryType: "intersects", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][]float64{{2.5, 2.5}, {4.5, 4.5}}, + DocShapeName: "linestring1", + Desc: "linestring outside bounded rectangle", + Expected: nil, + QueryType: "intersects", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][]float64{{0.25, 0.25}, {4.5, 4.5}}, + DocShapeName: "linestring1", + Desc: "linestring partially in bounded rectangle", + Expected: []string{"linestring1"}, + QueryType: "intersects", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopePolygonIntersect(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][][]float64{{{0.5, 0.5}, {1.5, 0.5}, {1.5, 1.5}, {0.5, 1.5}, {0.5, 0.5}}}, + DocShapeName: "polygon1", + Desc: "polygon intersects bounded rectangle", + Expected: []string{"polygon1"}, + QueryType: "intersects", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][][]float64{{{10.5, 10.5}, {11.5, 10.5}, {11.5, 11.5}, {10.5, 11.5}, {10.5, 10.5}}}, + DocShapeName: "polygon1", + Desc: "polygon completely outside bounded rectangle", + Expected: nil, + QueryType: "intersects", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPointIntersects(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][]float64{{3.0, 3.0}, {4.0, 4.0}}, + DocShapeVertices: [][]float64{{4.0, 4.0}}, + DocShapeName: "multipoint1", + Desc: "single coincident multipoint", + Expected: []string{"multipoint1"}, + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipoint", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + true, indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipoint: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][]float64{{3.0, 2.0}, {4.0, 2.0}}, + DocShapeVertices: [][]float64{{3.0, 2.0}, {4.0, 2.0}}, + DocShapeName: "linestring1", + Desc: "coincident linestrings", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {1.5, 1.5}, {2.0, 2.0}}, + DocShapeVertices: [][]float64{{2.0, 2.0}, {4.0, 3.0}}, + DocShapeName: "linestring1", + Desc: "linestrings intersecting at the ends", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {3.0, 3.0}}, + DocShapeVertices: [][]float64{{1.5499860, 1.5501575}, {4.0, 6.0}}, + DocShapeName: "linestring1", + Desc: "subline not at vertex", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {2.0, 2.0}}, + DocShapeVertices: [][]float64{{1.5499860, 1.5501575}, {1.5, 1.5001714}}, + DocShapeName: "linestring1", + Desc: "subline inside linestring", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {1.5, 1.5}, {2.0, 2.0}}, + DocShapeVertices: [][]float64{{1.0, 2.0}, {2.0, 1.0}}, + DocShapeName: "linestring1", + Desc: "linestrings intersecting at some edge", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {1.5, 1.5}, {2.0, 2.0}}, + DocShapeVertices: [][]float64{{1.0, 2.0}, {1.0, 4.0}}, + DocShapeName: "linestring1", + Desc: "non intersecting linestrings", + Expected: nil, + }, + { + QueryShape: [][]float64{{59.32, 0.52}, {68.99, -7.36}, {75.49, -12.21}}, + DocShapeVertices: [][]float64{{71.98, 0}, {67.58, -6.57}, {63.19, -12.72}}, + DocShapeName: "linestring1", + Desc: "linestrings with more than 2 points intersecting at some edges", + Expected: []string{"linestring1"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeLinestringQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestLinestringPolygonIntersects(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][]float64{{1.0, 1.0}, {1.5, 1.5}, {2.0, 2.0}}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}}, + DocShapeName: "polygon1", + Desc: "linestring intersects polygon at a vertex", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][]float64{{0.2, 0.2}, {0.4, 0.4}}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}}, + DocShapeName: "polygon1", + Desc: "linestring within polygon", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][]float64{{-0.5, 0.5}, {0.5, 0.5}}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}}, + DocShapeName: "polygon1", + Desc: "linestring intersects polygon at an edge", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][]float64{{-0.5, 0.5}, {1.5, 0.5}}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}}, + DocShapeName: "polygon1", + Desc: "linestring intersects polygon as a whole", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][]float64{{-0.5, 0.5}, {-1.5, -1.5}}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}}, + DocShapeName: "polygon1", + Desc: "linestring does not intersect polygon", + Expected: nil, + }, + { + QueryShape: [][]float64{{0.3, 0.3}, {0.35, 0.35}}, + DocShapeVertices: [][][]float64{ + {{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}, + {{0.2, 0.2}, {0.2, 0.4}, {0.4, 0.4}, {0.4, 0.2}, {0.2, 0.2}}, + }, + DocShapeName: "polygon1", + Desc: "linestring does not intersect polygon when contained in the hole", + Expected: nil, + }, + { + QueryShape: [][]float64{{0.3, 0.3}, {0.5, 0.5}}, + DocShapeVertices: [][][]float64{ + {{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}, + {{0.2, 0.2}, {0.2, 0.4}, {0.4, 0.4}, {0.4, 0.2}, {0.2, 0.2}}, + }, + DocShapeName: "polygon1", + Desc: "linestring intersects polygon in the hole", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][]float64{{0.4, 0.3}, {0.6, 0.3}}, + DocShapeVertices: [][][]float64{ + {{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}, + {{0.3, 0.3}, {0.4, 0.2}, {0.5, 0.3}, {0.4, 0.4}, {0.3, 0.3}}, + {{0.5, 0.3}, {0.6, 0.2}, {0.7, 0.3}, {0.6, 0.4}, {0.5, 0.3}}, + }, + DocShapeName: "polygon1", + Desc: "linestring intersects polygon through touching holes", + Expected: []string{"polygon1"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeLinestringQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for linestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestLinestringPointIntersects(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices []float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][]float64{{179, 0}, {-179, 0}}, + DocShapeVertices: []float64{179.1, 0}, + DocShapeName: "point1", + Desc: "point across longitudinal boundary of linestring", + Expected: []string{"point1"}, + }, + { + QueryShape: [][]float64{{-179, 0}, {179, 0}}, + DocShapeVertices: []float64{179.1, 0}, + DocShapeName: "point1", + Desc: "point across longitudinal boundary of reversed linestring", + Expected: []string{"point1"}, + }, + { + QueryShape: [][]float64{{179, 0}, {-179, 0}}, + DocShapeVertices: []float64{170, 0}, + DocShapeName: "point1", + Desc: "point does not intersect linestring", + Expected: nil, + }, + { + QueryShape: [][]float64{{-179, 0}, {179, 0}}, + DocShapeVertices: []float64{170, 0}, + DocShapeName: "point1", + Desc: "point does not intersect reversed linestring", + Expected: nil, + }, + { + QueryShape: [][]float64{{-179, 0}, {179, 0}, {178, 0}}, + DocShapeVertices: []float64{178, 0}, + DocShapeName: "point1", + Desc: "point intersects linestring at end vertex", + Expected: []string{"point1"}, + }, + { + QueryShape: [][]float64{{-179, 0}, {179, 0}, {178, 0}, {180, 0}}, + DocShapeVertices: []float64{178, 0}, + DocShapeName: "point1", + Desc: "point intersects linestring with more than two points", + Expected: []string{"point1"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeLinestringQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for linestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][]float64{{{1.0, 1.0}, {1.1, 1.1}, {2.0, 2.0}, {2.1, 2.1}}}, + DocShapeVertices: [][][]float64{{{0.0, 0.5132}, {-1.1, -1.1}, {1.5, 1.512}, {2.1, 2.1}}}, + DocShapeName: "multilinestring1", + Desc: "intersecting multilinestrings", + Expected: []string{"multilinestring1"}, + }, + { + QueryShape: [][][]float64{{{1.0, 1.0}, {1.1, 1.1}, {2.0, 2.0}, {2.1, 2.1}}}, + DocShapeVertices: [][][]float64{{{10.1, 100.5}, {11.5, 102.5}}}, + DocShapeName: "multilinestring1", + Desc: "non-intersecting multilinestrings", + Expected: nil, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multilinestring", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiLinestringQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multilinestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiLinestringMultiPointIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][]float64{{{2.0, 2.0}, {2.1, 2.1}}, {{3.0, 3.0}, {3.1, 3.1}}}, + DocShapeVertices: [][]float64{{5.0, 6.0}, {67, 67}, {3.1, 3.1}}, + DocShapeName: "multipoint1", + Desc: "multilinestring intersects one of the multipoints", + Expected: []string{"multipoint1"}, + }, + { + QueryShape: [][][]float64{{{2.0, 2.0}, {2.1, 2.1}}, {{3.0, 3.0}, {3.1, 3.1}}}, + DocShapeVertices: [][]float64{{56.0, 56.0}, {66, 66}}, + DocShapeName: "multipoint1", + Desc: "multilinestring does not intersect any of the multipoints", + Expected: nil, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipoint", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiLinestringQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][]float64{{ + {1.0, 1.0}, + {2.0, 1.0}, + {2.0, 2.0}, + {1.0, 2.0}, + {1.0, 1.0}, + }}, + DocShapeVertices: [][][]float64{{ + {1.0, 1.0}, + {2.0, 1.0}, + {2.0, 2.0}, + {1.0, 2.0}, + {1.0, 1.0}, + }}, + DocShapeName: "polygon1", + Desc: "coincident polygons", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][][]float64{{ + {1.0, 1.0}, + {2.0, 1.0}, + {2.0, 2.0}, + {1.0, 2.0}, + {1.0, 1.0}, + }}, + DocShapeVertices: [][][]float64{{ + {1.2, 1.2}, + {2.0, 1.0}, + {2.0, 2.0}, + {1.0, 2.0}, + {1.2, 1.2}, + }}, + DocShapeName: "polygon1", + Desc: "polygon and a window polygon", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][][]float64{{ + {1.0, 1.0}, + {2.0, 1.0}, + {2.0, 2.0}, + {1.0, 2.0}, + {1.0, 1.0}, + }}, + DocShapeVertices: [][][]float64{{ + {1.1, 1.1}, + {1.2, 1.1}, + {1.2, 1.2}, + {1.1, 1.2}, + {1.1, 1.1}, + }}, + DocShapeName: "polygon1", + Desc: "nested polygons", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][][]float64{{ + {1.0, 1.0}, + {2.0, 1.0}, + {2.0, 2.0}, + {1.0, 2.0}, + {1.0, 1.0}, + }}, + DocShapeVertices: [][][]float64{{ + {0.0, 1.0}, + {2.0, 1.0}, + {2.0, 2.0}, + {0.0, 2.0}, + {0.0, 1.0}, + }}, + DocShapeName: "polygon1", + Desc: "intersecting polygons", + Expected: []string{"polygon1"}, + }, + { + QueryShape: [][][]float64{{{0, 0}, {5, 0}, {5, 5}, {0, 5}, {0, 0}}, { + {1, 4}, + {4, 4}, + {4, 1}, + {1, 1}, + {1, 4}, + }}, + DocShapeVertices: [][][]float64{{{2, 2}, {3, 2}, {3, 3}, {2, 3}, {2, 2}}}, + DocShapeName: "polygon1", + Desc: "polygon inside hole of a larger polygon", + Expected: nil, + }, + { + QueryShape: [][][]float64{{ + {1.0, 1.0}, + {2.0, 1.0}, + {2.0, 2.0}, + {1.0, 2.0}, + {1.0, 1.0}, + }}, + DocShapeVertices: [][][]float64{{ + {3.0, 3.0}, + {4.0, 3.0}, + {4.0, 4.0}, + {3.0, 4.0}, + {3.0, 3.0}, + }}, + DocShapeName: "polygon1", + Desc: "disjoint polygons", + Expected: nil, + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][]float64{{{150, 85}, {160, 85}, {-20, 85}, {-30, 85}, {150, 85}}}, + DocShapeVertices: [][]float64{{150, 85}, {160, 85}}, + DocShapeName: "linestring1", + Desc: "polygon intersects line along edge", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][]float64{{150, 85}, {160, 85}}, + DocShapeName: "linestring1", + Desc: "polygon not intersecting line", + Expected: nil, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][]float64{{0.2, 0.2}, {0.4, 0.4}}, + DocShapeName: "linestring1", + Desc: "polygon completely encloses line", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][]float64{{-0.5, 0.5}, {1.5, 0.5}}, + DocShapeName: "linestring1", + Desc: "line cuts through entire polygon", + Expected: []string{"linestring1"}, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][]float64{{-0.439, -0.318}, {0.4339, 0.335}}, + DocShapeName: "linestring1", + Desc: "line partially cuts through polygon", + Expected: []string{"linestring1"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonMultiLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][]float64{{{150, 85}, {160, 85}, {-20, 85}, {-30, 85}, {150, 85}}}, + DocShapeVertices: [][][]float64{{{150, 85}, {160, 85}}, {{0, 1}, {5, 10}}}, + DocShapeName: "multilinestring1", + Desc: "polygon intersects one line along edge", + Expected: []string{"multilinestring1"}, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{150, 85}, {160, 85}}}, + DocShapeName: "multilinestring1", + Desc: "polygon not intersecting any line", + Expected: nil, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{0.2, 0.2}, {0.4, 0.4}}}, + DocShapeName: "multilinestring1", + Desc: "polygon completely encloses line", + Expected: []string{"multilinestring1"}, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{-0.5, 0.5}, {1.5, 0.5}}}, + DocShapeName: "multilinestring1", + Desc: "line cuts through entire polygon", + Expected: []string{"multilinestring1"}, + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{-0.439, -0.318}, {0.4339, 0.335}}}, + DocShapeName: "multilinestring1", + Desc: "line partially cuts through polygon", + Expected: []string{"multilinestring1"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multilinestring", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonPointIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices []float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][]float64{{{150, 85}, {160, 85}, {-20, 85}, {-30, 85}, {150, 85}}}, + DocShapeVertices: []float64{150, 88}, + DocShapeName: "point1", + Desc: "polygon intersects point in latitudinal boundary", + Expected: []string{"point1"}, + }, + { + QueryShape: [][][]float64{{{150, 85}, {160, 85}, {-20, 85}, {-30, 85}, {150, 85}}}, + DocShapeVertices: []float64{170, 88}, + DocShapeName: "point1", + Desc: "polygon does not intersects point outside latitudinal boundary", + Expected: nil, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPolygonIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][][]float64 + DocShapeVertices [][][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][][]float64{{{ + {15, 5}, + {40, 10}, + {10, 20}, + {5, 10}, + {15, 5}, + }}, {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}}, + DocShapeVertices: [][][][]float64{{{ + {0.0, 0.0}, + {1.0, 0.0}, + {1.0, 1.0}, + {0.0, 1.0}, + {0.0, 0.0}, + }, {{30, 20}, {45, 40}, {10, 40}, {30, 20}}}}, + DocShapeName: "multipolygon1", + Desc: "intersecting multi polygons", + Expected: []string{"multipolygon1"}, + }, + { + QueryShape: [][][][]float64{{{ + {15, 5}, + {40, 10}, + {10, 20}, + {5, 10}, + {15, 5}, + }}, {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}}, + DocShapeVertices: [][][][]float64{{{ + {0.0, 0.0}, + {1.0, 0.0}, + {1.0, 1.0}, + {0.0, 1.0}, + {0.0, 0.0}, + }}}, + DocShapeName: "multipolygon1", + Desc: "non intersecting multi polygons", + Expected: nil, + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipolygon", + test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiPolygonQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipolygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPolygonMultiPointIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][][]float64{ + {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}, + {{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, + }, + DocShapeVertices: [][]float64{{30, 20}, {30, 30}}, + DocShapeName: "multipoint1", + Desc: "multipolygon intersects multipoint at the vertex", + Expected: []string{"multipoint1"}, + }, + { + QueryShape: [][][][]float64{ + {{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, + {{{30, 20}, {45, 50}, {10, 50}, {30, 20}}}, + }, + DocShapeVertices: [][]float64{{30, -20}, {-30, 30}, {45, 66}}, + DocShapeName: "multipoint1", + Desc: "multipolygon does not intersect multipoint", + Expected: nil, + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipoint", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiPolygonQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipolygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPolygonMultiLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + }{ + { + QueryShape: [][][][]float64{{{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}}, + DocShapeVertices: [][][]float64{{{65, 40}, {60, 40}}, {{45, 40}, {10, 40}, {30, 20}}}, + DocShapeName: "multilinestring1", + Desc: "multipolygon intersects multilinestring", + Expected: []string{"multilinestring1"}, + }, + { + QueryShape: [][][][]float64{{{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}}, + DocShapeVertices: [][][]float64{{{45, 41}, {60, 80}}, {{-45, -40}, {-10, -40}}}, + DocShapeName: "multilinestring1", + Desc: "multipolygon does not intersect multilinestring", + Expected: nil, + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multilinestring", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiPolygonQueryWithRelation("intersects", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipolygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][][][]float64 + DocShapeVertices [][][][][]float64 + DocShapeName string + Desc string + Expected []string + Types []string + }{ + { + QueryShape: [][][][][]float64{{{{}}}}, + DocShapeVertices: [][][][][]float64{{{{}}}}, + DocShapeName: "geometrycollection1", + Desc: "empty geometry collections", + Expected: nil, + Types: []string{""}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetupGeometryCollection(t, test.DocShapeName, test.Types, + test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeGeometryCollectionRelationQuery("intersects", + indexReader, test.QueryShape, test.Types, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for geometry collection: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionPointIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][][][]float64 + DocShapeVertices []float64 + DocShapeName string + Desc string + Expected []string + Types []string + }{ + { + QueryShape: [][][][][]float64{{{{{4, 5}}}}}, + DocShapeVertices: []float64{4, 5}, + DocShapeName: "point1", + Desc: "point coincident with point in geometry collection", + Expected: []string{"point1"}, + Types: []string{"point"}, + }, + { + QueryShape: [][][][][]float64{{{{{4, 5}, {6, 7}}}}}, + DocShapeVertices: []float64{4, 5}, + DocShapeName: "point1", + Desc: "point on vertex of linestring in geometry collection", + Expected: []string{"point1"}, + Types: []string{"linestring"}, + }, + { + QueryShape: [][][][][]float64{{{{{1, 1}, {2, 2}, {0, 2}, {1, 0}}, {{5, 6}}}}}, + DocShapeVertices: []float64{1.5, 1.9}, + DocShapeName: "point1", + Desc: "point inside polygon in geometry collection", + Expected: []string{"point1"}, + Types: []string{"polygon", "point"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Fatal(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeGeometryCollectionRelationQuery("intersects", + indexReader, test.QueryShape, test.Types, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for geometry collection: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionLinestringIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + Types []string + }{ + { + QueryShape: [][][][][]float64{{{{{4, 5}, {6, 7}, {7, 8}}}}}, + DocShapeVertices: [][]float64{{6, 7}, {7, 8}}, + DocShapeName: "linestring1", + Desc: "linestring intersecting with linestring in geometry collection", + Expected: []string{"linestring1"}, + Types: []string{"linestring"}, + }, + { + QueryShape: [][][][][]float64{{{{{1.5, 1.9}}}}}, + DocShapeVertices: [][]float64{{1.5, 1.9}, {2.5, 2.8}}, + DocShapeName: "linestring1", + Desc: "linestring intersects point in geometry collection at vertex", + Expected: []string{"linestring1"}, + Types: []string{"point"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeGeometryCollectionRelationQuery("intersects", + indexReader, test.QueryShape, test.Types, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for geometry collection: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionPolygonIntersects(t *testing.T) { + tests := []struct { + QueryShape [][][][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + Types []string + }{ + { + QueryShape: [][][][][]float64{{{{{4, 5}, {6, 7}, {7, 8}, {4, 5}}}, {{{1, 2}, {2, 3}, {3, 4}, {1, 2}}}}}, + DocShapeVertices: [][][]float64{{{4, 5}, {6, 7}, {7, 8}, {4, 5}}}, + DocShapeName: "polygon1", + Desc: "polygon coincides with one of the polygons in multipolygon in geometry collection", + Expected: []string{"polygon1"}, + Types: []string{"multipolygon"}, + }, + { + QueryShape: [][][][][]float64{{{{{14, 15}}}}}, + DocShapeVertices: [][][]float64{{{4, 5}, {6, 7}, {7, 8}, {4, 5}}}, + DocShapeName: "polygon1", + Desc: "polygon does not intersect point in geometry collection", + Expected: nil, + Types: []string{"point"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeGeometryCollectionRelationQuery("intersects", + indexReader, test.QueryShape, test.Types, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for geometry collection: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointGeometryCollectionIntersects(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][][][][]float64 + DocShapeName string + Desc string + Expected []string + Types []string + }{ + { + QueryShape: []float64{1.0, 2.0}, + DocShapeVertices: [][][][][]float64{{{{}}}}, + DocShapeName: "geometrycollection1", + Desc: "geometry collection does not intersect with a point", + Expected: nil, + Types: []string{""}, + }, + { + QueryShape: []float64{1.0, 2.0}, + DocShapeVertices: [][][][][]float64{{{{{1.0, 2.0}}}}}, + DocShapeName: "geometrycollection1", + Desc: "geometry collection intersects with a point", + Expected: []string{"geometrycollection1"}, + Types: []string{"point"}, + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetupGeometryCollection(t, test.DocShapeName, test.Types, + test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} diff --git a/search/searcher/geoshape_within_test.go b/search/searcher/geoshape_within_test.go new file mode 100644 index 0000000..6f93653 --- /dev/null +++ b/search/searcher/geoshape_within_test.go @@ -0,0 +1,1351 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "fmt" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + index "github.com/blevesearch/bleve_index_api" +) + +var ( + leftRect [][][]float64 = [][][]float64{{{-1, 0}, {0, 0}, {0, 1}, {-1, 1}, {-1, 0}}} + leftRectPoint []float64 = []float64{-0.5, 0.5} + rightRect [][][]float64 = [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}} + rightRectPoint []float64 = []float64{0.5, 0.5} +) + +func testCaseSetupGeometryCollection(t *testing.T, docShapeName string, types []string, docShapeVertices [][][][][]float64, + i index.Index, +) (index.IndexReader, func() error, error) { + doc := document.NewDocument(docShapeName) + gcField := document.NewGeometryCollectionFieldWithIndexingOptions("geometry", + []uint64{}, docShapeVertices, types, document.DefaultGeoShapeIndexingOptions) + if gcField == nil { + return nil, nil, fmt.Errorf("the GC field is nil") + } + doc.AddField(gcField) + if doc == nil { + return nil, nil, fmt.Errorf("the doc is nil") + } + err := i.Update(doc) + if err != nil { + t.Error(err.Error()) + } + + indexReader, err := i.Reader() + if err != nil { + t.Fatal(err) + } + + closeFn := func() error { + err = i.Delete(doc.ID()) + if err != nil { + return err + } + err = indexReader.Close() + if err != nil { + return err + } + return nil + } + + return indexReader, closeFn, nil +} + +func TestPointWithin(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices []float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: []float64{1.0, 1.0}, + DocShapeVertices: []float64{1.0, 1.0}, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point contains itself", + QueryType: "within", + }, + { + QueryShape: []float64{1.0, 1.0}, + DocShapeVertices: []float64{1.0, 1.1}, + DocShapeName: "point1", + Expected: nil, + Desc: "point does not contain a different point", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPointWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][]float64{{1.0, 1.0}, {2.0, 2.0}}, + DocShapeVertices: [][]float64{{1.0, 1.0}}, + DocShapeName: "multipoint1", + Expected: []string{"multipoint1"}, + Desc: "single multipoint common", + QueryType: "within", + }, + { + QueryShape: [][]float64{{1.0, 1.0}}, + DocShapeVertices: [][]float64{{1.0, 1.0}, {2.0, 2.0}}, + DocShapeName: "multipoint1", + Expected: nil, + Desc: "multipoint not covered by multiple multipoints", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipoint", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + true, indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipoint: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopePointWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices []float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: []float64{0.5, 0.5}, + DocShapeName: "point1", + Desc: "point completely within bounded rectangle", + Expected: []string{"point1"}, + QueryType: "within", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: []float64{0, 1}, + DocShapeName: "point1", + Desc: "point on vertex of bounded rectangle", + Expected: []string{"point1"}, + QueryType: "within", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: []float64{10, 11}, + DocShapeName: "point1", + Desc: "point outside bounded rectangle", + Expected: nil, + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopeLinestringWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][]float64{{0.5, 0.5}, {0.75, 0.75}}, + DocShapeName: "linestring1", + Desc: "linestring completely within bounded rectangle", + Expected: []string{"linestring1"}, + QueryType: "within", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][]float64{{0.5, 0.5}, {1.75, 1.75}}, + DocShapeName: "linestring1", + Desc: "linestring partially within bounded rectangle", + Expected: nil, + QueryType: "within", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][]float64{{1.5, 2.5}, {2.75, 2.75}}, + DocShapeName: "linestring1", + Desc: "linestring completely outside bounded rectangle", + Expected: nil, + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestEnvelopePolygonWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + }{ + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][][]float64{{{0.5, 0.5}, {1, 0.5}, {1, 1}, {0.5, 1}, {0.5, 0.5}}}, + DocShapeName: "polygon1", + Desc: "polygon completely within bounded rectangle", + Expected: nil, + QueryType: "within", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][][]float64{{{0.5, 0.5}, {1.5, 0.5}, {1.5, 1.5}, {0.5, 1.5}, {0.5, 0.5}}}, + DocShapeName: "polygon1", + Desc: "polygon partially within bounded rectangle", + Expected: nil, + QueryType: "within", + }, + { + QueryShape: [][]float64{{0, 1}, {1, 0}}, + DocShapeVertices: [][][]float64{{{10.5, 10.5}, {11.5, 10.5}, {11.5, 11.5}, {10.5, 11.5}, {10.5, 10.5}}}, + DocShapeName: "polygon1", + Desc: "polygon completely outside bounded rectangle", + Expected: nil, + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeEnvelopeRelationQuery(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for Envelope: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointLinestringWithin(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: []float64{1.0, 1.0}, + DocShapeVertices: [][]float64{{1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "point does not cover different linestring", + QueryType: "within", + }, + { + QueryShape: []float64{179.1, 0.0}, + DocShapeVertices: [][]float64{{-179.0, 0.0}, {179.0, 0.0}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "point across latitudinal boundary of linestring", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPointPolygonWithin(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: []float64{1.0, 1.0}, + DocShapeVertices: [][][]float64{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}}, + DocShapeName: "polygon1", + Expected: nil, + Desc: "point not within polygon", + QueryType: "within", + }, + { // from binary predicates file + QueryShape: rightRectPoint, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Expected: nil, // will return nil since a point only returns non-nil for a coincident point + // even if the point is on the polygon + Desc: "point on rectangle vertex", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestLinestringPointWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices []float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][]float64{{1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeVertices: []float64{1.0, 1.0}, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point at start of linestring", + QueryType: "within", + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeVertices: []float64{2.0, 2.0}, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point in the middle of linestring", + QueryType: "within", + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeVertices: []float64{3.0, 3.0}, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point at end of linestring", + QueryType: "within", + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeVertices: []float64{1.5, 1.50017}, + DocShapeName: "point1", + Expected: nil, + Desc: "point in between linestring", + QueryType: "within", + }, + { + QueryShape: [][]float64{{1.0, 1.0}, {2.0, 2.0}, {3.0, 3.0}}, + DocShapeVertices: []float64{4, 5}, + DocShapeName: "point1", + Expected: nil, + Desc: "point not contained by linestring", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeLinestringQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for linestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPointMultiLinestringWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][]float64{{2, 2}, {2.1, 2.1}}, + DocShapeVertices: [][][]float64{{{1, 1}, {1.1, 1.1}}, {{2, 2}, {2.1, 2.1}}}, + DocShapeName: "multilinestring1", + Expected: nil, // nil since multipoint within multiline is always nil + Desc: "multilinestring covering multipoint", + QueryType: "within", + }, + { + QueryShape: [][]float64{{2, 2}, {1, 1}, {3, 3}}, + DocShapeVertices: [][][]float64{{{1, 1}, {1.1, 1.1}}, {{2, 2}, {2.1, 2.1}}}, + DocShapeName: "multipoint1", + Expected: nil, + Desc: "multilinestring not covering multipoint", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multilinestring", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery(test.QueryType, + true, indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multilinestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestLinestringWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][]float64{{1, 1}, {2, 2}, {3, 3}}, + DocShapeVertices: [][]float64{{1, 1}, {2, 2}, {3, 3}, {4, 4}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "longer linestring", + QueryType: "within", + }, + { + QueryShape: [][]float64{{1, 1}, {2, 2}, {3, 3}}, + DocShapeVertices: [][]float64{{1, 1}, {2, 2}, {3, 3}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "coincident linestrings", + QueryType: "within", + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeLinestringQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for linestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestLinestringGeometryCollectionWithin(t *testing.T) { + tests := []struct { + QueryShape [][]float64 + DocShapeVertices [][][][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + Types []string + }{ + { + QueryShape: [][]float64{{1, 1}, {2, 2}}, + DocShapeVertices: [][][][][]float64{{{{{1, 1}}}}}, + DocShapeName: "geometrycollection1", + Expected: nil, // LS is not a closed shape + Desc: "geometry collection with a point on vertex of linestring", + Types: []string{"point"}, + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetupGeometryCollection(t, test.DocShapeName, test.Types, + test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeLinestringQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for linestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonPointWithin(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices []float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: []float64{0.5, 0.5}, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point within polygon", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: []float64{5.5, 5.5}, + DocShapeName: "point1", + Expected: nil, + Desc: "point not within polygon", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{ + {0, 0}, + {1, 0}, + {1, 1}, + {0, 1}, + {0, 0}, + {0.2, 0.2}, + {0.2, 0.4}, + {0.4, 0.4}, + {0.4, 0.2}, + {0.2, 0.2}, + }}, + DocShapeVertices: []float64{0.3, 0.3}, + DocShapeName: "point1", + Expected: nil, + Desc: "point within polygon hole", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.0}}}, + DocShapeVertices: []float64{1.0, 0.0}, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point on polygon vertex", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{1, 1}, {2, 2}, {0, 2}, {1, 1}}}, + DocShapeVertices: []float64{1.5, 1.5001714}, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point inside polygon", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{150, 85}, {-20, -85}, {-30, 85}, {160, -85}, {150, 85}}}, + DocShapeVertices: []float64{170, 85}, + DocShapeName: "point1", + Expected: nil, + Desc: "point outside the polygon's latitudinal boundary", + QueryType: "within", + }, + { + // from binary predicates tests + QueryShape: leftRect, + DocShapeVertices: leftRectPoint, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point in left rectangle", + QueryType: "within", + }, + { + // from binary predicates tests + QueryShape: rightRect, + DocShapeVertices: rightRectPoint, + DocShapeName: "point1", + Expected: []string{"point1"}, + Desc: "point in right rectangle", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "point", + [][][][]float64{{{test.DocShapeVertices}}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonLinestringWithin(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][]float64{{0.1, 0.1}, {0.4, 0.4}}, + DocShapeName: "linestring1", + Expected: []string{"linestring1"}, + Desc: "linestring within polygon", + QueryType: "within", + }, + { + QueryShape: [][][]float64{ + {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}, + {{0.2, 0.2}, {0.2, 0.4}, {0.4, 0.4}, {0.4, 0.2}, {0.2, 0.2}}, + }, + DocShapeVertices: [][]float64{{0.3, 0.3}, {0.55, 0.55}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "linestring intersecting with polygon hole", + QueryType: "within", + }, + { + QueryShape: [][][]float64{ + {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}, + {{0.2, 0.2}, {0.2, 0.4}, {0.4, 0.4}, {0.4, 0.2}, {0.2, 0.2}}, + }, + DocShapeVertices: [][]float64{{0.3, 0.3}, {4.0, 4.0}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "linestring intersecting with polygon hole and outside", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][]float64{{-1, -1}, {-2, -2}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "linestring outside polygon", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][]float64{{-0.5, -0.5}, {0.5, 0.5}}, + DocShapeName: "linestring1", + Expected: nil, + Desc: "linestring intersecting polygon", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "linestring", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestPolygonWithin(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "coincident polygon", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{0.2, 0.2}, {1, 0}, {1, 1}, {0, 1}, {0.2, 0.2}}}, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "polygon covers an intersecting window of itself", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{0.1, 0.1}, {0.2, 0.1}, {0.2, 0.2}, {0.1, 0.2}, {0.1, 0.1}}}, + DocShapeName: "polygon1", + Expected: []string{"polygon1"}, + Desc: "polygon covers a nested version of itself", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{-1, 0}, {1, 0}, {1, 1}, {-1, 1}, {-1, 0}}}, + DocShapeName: "polygon1", + Expected: nil, + Desc: "intersecting polygons", + QueryType: "within", + }, + { + QueryShape: [][][]float64{{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + DocShapeVertices: [][][]float64{{{3, 3}, {4, 3}, {4, 4}, {3, 4}, {3, 3}}}, + DocShapeName: "polygon1", + Expected: nil, + Desc: "polygon totally out of range", + QueryType: "within", + }, + { + QueryShape: leftRect, + DocShapeVertices: rightRect, + DocShapeName: "polygon1", + Expected: nil, + Desc: "left and right polygons,sharing an edge", + QueryType: "within", + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "polygon", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPolygonMultiPointWithin(t *testing.T) { + tests := []struct { + QueryShape [][][][]float64 + DocShapeVertices [][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][][]float64{ + {{{30, 25}, {45, 40}, {10, 40}, {30, 20}, {30, 25}}}, + {{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, + }, + DocShapeVertices: [][]float64{{30, 20}, {15, 5}}, + DocShapeName: "multipoint1", + Expected: []string{"multipoint1"}, + Desc: "multipolygon covers multipoint", + QueryType: "within", + }, + { + QueryShape: [][][][]float64{ + {{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, + {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}, + }, + DocShapeVertices: [][]float64{{30, 20}, {30, 30}, {45, 66}}, + DocShapeName: "multipoint1", + Expected: nil, + Desc: "multipolygon does not cover multipoint", + QueryType: "within", + }, + { + QueryShape: [][][][]float64{ + {{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}}, + {{{1, 0}, {2, 0}, {2, 1}, {1, 1}, {1, 0}}}, + }, + DocShapeVertices: [][]float64{{0.5, 0.5}, {1.5, 0.5}}, + DocShapeName: "multipoint1", + Expected: []string{"multipoint1"}, + Desc: "multiple multipolygons required to cover multipoint", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipoint", + [][][][]float64{{test.DocShapeVertices}}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiPolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for polygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiLinestringWithin(t *testing.T) { + tests := []struct { + QueryShape [][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][]float64{{{1, 2}, {2, 3}, {3, 4}}, {{5, 6}, {6.5, 7.8}}}, + DocShapeVertices: [][][]float64{{{1, 2}, {2, 3}, {3, 4}}}, + DocShapeName: "multilinestring1", + Expected: nil, + Desc: "multilinestrings with common linestrings", + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multilinestring", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiLinestringQueryWithRelation("within", + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multilinestring: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPolygonMultiLinestringWithin(t *testing.T) { + tests := []struct { + QueryShape [][][][]float64 + DocShapeVertices [][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][][]float64{ + {{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, + {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}, + }, + DocShapeVertices: [][][]float64{{{45, 40}, {10, 40}}, {{45, 40}, {10, 40}, {30, 20}}}, + DocShapeName: "multilinestring1", + Expected: []string{"multilinestring1"}, + Desc: "multilinestring intersecting at the edge of multipolygon", + QueryType: "within", + }, + { + QueryShape: [][][][]float64{ + {{{15, 5}, {40, 10}, {10, 20}, {5, 10}, {15, 5}}}, + {{{30, 20}, {45, 40}, {10, 40}, {30, 20}}}, + }, + DocShapeVertices: [][][]float64{{{48, 40}, {8, 40}}, {{48, 40}, {8, 40}, {30, 12}}}, + DocShapeName: "multilinestring1", + Expected: nil, + Desc: "multipolygon does not cover multilinestring", + QueryType: "within", + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multilinestring", + [][][][]float64{test.DocShapeVertices}, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiPolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipolygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestMultiPolygonWithin(t *testing.T) { + tests := []struct { + QueryShape [][][][]float64 + DocShapeVertices [][][][]float64 + DocShapeName string + Expected []string + Desc string + QueryType string + }{ + { + QueryShape: [][][][]float64{ + {{{16, 6}, {41, 11}, {11, 21}, {6, 11}, {16, 6}}}, + {{{31, 21}, {46, 41}, {11, 41}, {31, 21}}}, + }, + DocShapeVertices: [][][][]float64{{{{31, 21}, {46, 41}, {11, 41}, {31, 21}}}}, + DocShapeName: "multipolygon1", + Expected: []string{"multipolygon1"}, + Desc: "multipolygon covers another multipolygon", + QueryType: "within", + }, + { + QueryShape: [][][][]float64{ + {{{16, 6}, {41, 11}, {11, 21}, {6, 11}, {16, 6}}}, + {{{31, 21}, {46, 41}, {11, 41}, {31, 21}}}, + }, + DocShapeVertices: [][][][]float64{{{{31, 21}, {46, 41}, {16, 46}, {31, 21}}}}, + DocShapeName: "multipolygon1", + Expected: nil, + Desc: "multipolygon does not cover multipolygon", + QueryType: "within", + }, + } + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetup(t, test.DocShapeName, "multipolygon", + test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeMultiPolygonQueryWithRelation(test.QueryType, + indexReader, test.QueryShape, "geometry") + if err != nil { + t.Error(err.Error()) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for multipolygon: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionWithin(t *testing.T) { + tests := []struct { + QueryShape [][][][][]float64 + DocShapeVertices [][][][][]float64 + DocShapeName string + Desc string + Expected []string + QueryType string + QueryShapeTypes []string + DocShapeTypes []string + }{ + { + QueryShape: [][][][][]float64{{{{}}}}, + DocShapeVertices: [][][][][]float64{{{{}}}}, + DocShapeName: "geometrycollection1", + Desc: "empty geometry collections", + Expected: nil, + QueryType: "within", + QueryShapeTypes: []string{""}, + DocShapeTypes: []string{""}, + }, + { + QueryShape: [][][][][]float64{{{{{1, 2}, {2, 3}}}}}, + DocShapeVertices: [][][][][]float64{{{{{1, 2}}}}}, + DocShapeName: "geometrycollection1", + Desc: "geometry collection with a linestring", + Expected: []string{"geometrycollection1"}, + QueryShapeTypes: []string{"linestring"}, + DocShapeTypes: []string{"point"}, + QueryType: "within", + }, + { + QueryShape: [][][][][]float64{{{{{1, 2}, {2, 3}, {5, 6}}}}}, + DocShapeVertices: [][][][][]float64{{{{{1, 2}}}}}, + DocShapeName: "geometrycollection1", + Desc: "geometry collections with common points and multipoints", + Expected: []string{"geometrycollection1"}, + QueryShapeTypes: []string{"multipoint"}, + DocShapeTypes: []string{"point"}, + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetupGeometryCollection(t, test.DocShapeName, test.DocShapeTypes, + test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapeGeometryCollectionRelationQuery(test.QueryType, + indexReader, test.QueryShape, test.QueryShapeTypes, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for geometry collection: %+v", + test.Expected, got, test.QueryShape) + } + }) + + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} + +func TestGeometryCollectionPointWithin(t *testing.T) { + tests := []struct { + QueryShape []float64 + DocShapeVertices [][][][][]float64 + DocShapeName string + Desc string + Expected []string + Types []string + QueryType string + }{ + { + QueryShape: []float64{1.0, 2.0}, + DocShapeVertices: [][][][][]float64{{{{}}}}, + DocShapeName: "geometrycollection1", + Desc: "empty geometry collection not within a point", + Expected: nil, + Types: []string{""}, + QueryType: "within", + }, + } + + i := setupIndex(t) + + for _, test := range tests { + indexReader, closeFn, err := testCaseSetupGeometryCollection(t, test.DocShapeName, test.Types, + test.DocShapeVertices, i) + if err != nil { + t.Error(err.Error()) + } + + t.Run(test.Desc, func(t *testing.T) { + got, err := runGeoShapePointRelationQuery("intersects", + false, indexReader, [][]float64{test.QueryShape}, "geometry") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.Expected) { + t.Errorf("expected %v, got %v for point: %+v", + test.Expected, got, test.QueryShape) + } + }) + err = closeFn() + if err != nil { + t.Error(err.Error()) + } + } +} diff --git a/search/searcher/optimize_knn.go b/search/searcher/optimize_knn.go new file mode 100644 index 0000000..efe262b --- /dev/null +++ b/search/searcher/optimize_knn.go @@ -0,0 +1,53 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package searcher + +import ( + "context" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func optimizeKNN(ctx context.Context, indexReader index.IndexReader, + qsearchers []search.Searcher) error { + var octx index.VectorOptimizableContext + var err error + + for _, searcher := range qsearchers { + // Only applicable to KNN Searchers. + o, ok := searcher.(index.VectorOptimizable) + if !ok { + continue + } + + octx, err = o.VectorOptimize(ctx, octx) + if err != nil { + return err + } + } + + // No KNN searchers. + if octx == nil { + return nil + } + + // Postings lists and iterators replaced in the pointer to the + // vector reader + return octx.Finish() +} diff --git a/search/searcher/optimize_no_knn.go b/search/searcher/optimize_no_knn.go new file mode 100644 index 0000000..bd5d91f --- /dev/null +++ b/search/searcher/optimize_no_knn.go @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !vectors +// +build !vectors + +package searcher + +import ( + "context" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func optimizeKNN(ctx context.Context, indexReader index.IndexReader, + qsearchers []search.Searcher) error { + // No-op + return nil +} diff --git a/search/searcher/ordered_searchers_list.go b/search/searcher/ordered_searchers_list.go new file mode 100644 index 0000000..ac9da56 --- /dev/null +++ b/search/searcher/ordered_searchers_list.go @@ -0,0 +1,55 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "github.com/blevesearch/bleve/v2/search" +) + +type OrderedSearcherList []search.Searcher + +// sort.Interface + +func (otrl OrderedSearcherList) Len() int { + return len(otrl) +} + +func (otrl OrderedSearcherList) Less(i, j int) bool { + return otrl[i].Count() < otrl[j].Count() +} + +func (otrl OrderedSearcherList) Swap(i, j int) { + otrl[i], otrl[j] = otrl[j], otrl[i] +} + +type OrderedPositionalSearcherList struct { + searchers []search.Searcher + index []int +} + +// sort.Interface + +func (otrl OrderedPositionalSearcherList) Len() int { + return len(otrl.searchers) +} + +func (otrl OrderedPositionalSearcherList) Less(i, j int) bool { + return otrl.searchers[i].Count() < otrl.searchers[j].Count() +} + +func (otrl OrderedPositionalSearcherList) Swap(i, j int) { + otrl.searchers[i], otrl.searchers[j] = otrl.searchers[j], otrl.searchers[i] + otrl.index[i], otrl.index[j] = otrl.index[j], otrl.index[i] +} diff --git a/search/searcher/search_boolean.go b/search/searcher/search_boolean.go new file mode 100644 index 0000000..bf207f8 --- /dev/null +++ b/search/searcher/search_boolean.go @@ -0,0 +1,451 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeBooleanSearcher int + +func init() { + var bs BooleanSearcher + reflectStaticSizeBooleanSearcher = int(reflect.TypeOf(bs).Size()) +} + +type BooleanSearcher struct { + indexReader index.IndexReader + mustSearcher search.Searcher + shouldSearcher search.Searcher + mustNotSearcher search.Searcher + queryNorm float64 + currMust *search.DocumentMatch + currShould *search.DocumentMatch + currMustNot *search.DocumentMatch + currentID index.IndexInternalID + min uint64 + scorer *scorer.ConjunctionQueryScorer + matches []*search.DocumentMatch + initialized bool + done bool +} + +func NewBooleanSearcher(ctx context.Context, indexReader index.IndexReader, mustSearcher search.Searcher, shouldSearcher search.Searcher, mustNotSearcher search.Searcher, options search.SearcherOptions) (*BooleanSearcher, error) { + // build our searcher + rv := BooleanSearcher{ + indexReader: indexReader, + mustSearcher: mustSearcher, + shouldSearcher: shouldSearcher, + mustNotSearcher: mustNotSearcher, + scorer: scorer.NewConjunctionQueryScorer(options), + matches: make([]*search.DocumentMatch, 2), + } + rv.computeQueryNorm() + return &rv, nil +} + +func (s *BooleanSearcher) Size() int { + sizeInBytes := reflectStaticSizeBooleanSearcher + size.SizeOfPtr + + if s.mustSearcher != nil { + sizeInBytes += s.mustSearcher.Size() + } + + if s.shouldSearcher != nil { + sizeInBytes += s.shouldSearcher.Size() + } + + if s.mustNotSearcher != nil { + sizeInBytes += s.mustNotSearcher.Size() + } + + sizeInBytes += s.scorer.Size() + + for _, entry := range s.matches { + if entry != nil { + sizeInBytes += entry.Size() + } + } + + return sizeInBytes +} + +func (s *BooleanSearcher) computeQueryNorm() { + // first calculate sum of squared weights + sumOfSquaredWeights := 0.0 + if s.mustSearcher != nil { + sumOfSquaredWeights += s.mustSearcher.Weight() + } + if s.shouldSearcher != nil { + sumOfSquaredWeights += s.shouldSearcher.Weight() + } + + // now compute query norm from this + s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights) + // finally tell all the downstream searchers the norm + if s.mustSearcher != nil { + s.mustSearcher.SetQueryNorm(s.queryNorm) + } + if s.shouldSearcher != nil { + s.shouldSearcher.SetQueryNorm(s.queryNorm) + } +} + +func (s *BooleanSearcher) initSearchers(ctx *search.SearchContext) error { + var err error + // get all searchers pointing at their first match + if s.mustSearcher != nil { + if s.currMust != nil { + ctx.DocumentMatchPool.Put(s.currMust) + } + s.currMust, err = s.mustSearcher.Next(ctx) + if err != nil { + return err + } + } + + if s.shouldSearcher != nil { + if s.currShould != nil { + ctx.DocumentMatchPool.Put(s.currShould) + } + s.currShould, err = s.shouldSearcher.Next(ctx) + if err != nil { + return err + } + } + + if s.mustNotSearcher != nil { + if s.currMustNot != nil { + ctx.DocumentMatchPool.Put(s.currMustNot) + } + s.currMustNot, err = s.mustNotSearcher.Next(ctx) + if err != nil { + return err + } + } + + if s.mustSearcher != nil && s.currMust != nil { + s.currentID = s.currMust.IndexInternalID + } else if s.mustSearcher == nil && s.currShould != nil { + s.currentID = s.currShould.IndexInternalID + } else { + s.currentID = nil + } + + s.initialized = true + return nil +} + +func (s *BooleanSearcher) advanceNextMust(ctx *search.SearchContext, skipReturn *search.DocumentMatch) error { + var err error + + if s.mustSearcher != nil { + if s.currMust != skipReturn { + ctx.DocumentMatchPool.Put(s.currMust) + } + s.currMust, err = s.mustSearcher.Next(ctx) + if err != nil { + return err + } + } else { + if s.currShould != skipReturn { + ctx.DocumentMatchPool.Put(s.currShould) + } + s.currShould, err = s.shouldSearcher.Next(ctx) + if err != nil { + return err + } + } + + if s.mustSearcher != nil && s.currMust != nil { + s.currentID = s.currMust.IndexInternalID + } else if s.mustSearcher == nil && s.currShould != nil { + s.currentID = s.currShould.IndexInternalID + } else { + s.currentID = nil + } + return nil +} + +func (s *BooleanSearcher) Weight() float64 { + var rv float64 + if s.mustSearcher != nil { + rv += s.mustSearcher.Weight() + } + if s.shouldSearcher != nil { + rv += s.shouldSearcher.Weight() + } + + return rv +} + +func (s *BooleanSearcher) SetQueryNorm(qnorm float64) { + if s.mustSearcher != nil { + s.mustSearcher.SetQueryNorm(qnorm) + } + if s.shouldSearcher != nil { + s.shouldSearcher.SetQueryNorm(qnorm) + } +} + +func (s *BooleanSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + + if s.done { + return nil, nil + } + + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + + var err error + var rv *search.DocumentMatch + + for s.currentID != nil { + if s.currMustNot != nil { + cmp := s.currMustNot.IndexInternalID.Compare(s.currentID) + if cmp < 0 { + ctx.DocumentMatchPool.Put(s.currMustNot) + // advance must not searcher to our candidate entry + s.currMustNot, err = s.mustNotSearcher.Advance(ctx, s.currentID) + if err != nil { + return nil, err + } + if s.currMustNot != nil && s.currMustNot.IndexInternalID.Equals(s.currentID) { + // the candidate is excluded + err = s.advanceNextMust(ctx, nil) + if err != nil { + return nil, err + } + continue + } + } else if cmp == 0 { + // the candidate is excluded + err = s.advanceNextMust(ctx, nil) + if err != nil { + return nil, err + } + continue + } + } + + shouldCmpOrNil := 1 // NOTE: shouldCmp will also be 1 when currShould == nil. + if s.currShould != nil { + shouldCmpOrNil = s.currShould.IndexInternalID.Compare(s.currentID) + } + + if shouldCmpOrNil < 0 { + ctx.DocumentMatchPool.Put(s.currShould) + // advance should searcher to our candidate entry + s.currShould, err = s.shouldSearcher.Advance(ctx, s.currentID) + if err != nil { + return nil, err + } + if s.currShould != nil && s.currShould.IndexInternalID.Equals(s.currentID) { + // score bonus matches should + var cons []*search.DocumentMatch + if s.currMust != nil { + cons = s.matches + cons[0] = s.currMust + cons[1] = s.currShould + } else { + cons = s.matches[0:1] + cons[0] = s.currShould + } + rv = s.scorer.Score(ctx, cons) + err = s.advanceNextMust(ctx, rv) + if err != nil { + return nil, err + } + break + } else if s.shouldSearcher.Min() == 0 { + // match is OK anyway + cons := s.matches[0:1] + cons[0] = s.currMust + rv = s.scorer.Score(ctx, cons) + err = s.advanceNextMust(ctx, rv) + if err != nil { + return nil, err + } + break + } + } else if shouldCmpOrNil == 0 { + // score bonus matches should + var cons []*search.DocumentMatch + if s.currMust != nil { + cons = s.matches + cons[0] = s.currMust + cons[1] = s.currShould + } else { + cons = s.matches[0:1] + cons[0] = s.currShould + } + rv = s.scorer.Score(ctx, cons) + err = s.advanceNextMust(ctx, rv) + if err != nil { + return nil, err + } + break + } else if s.shouldSearcher == nil || s.shouldSearcher.Min() == 0 { + // match is OK anyway + cons := s.matches[0:1] + cons[0] = s.currMust + rv = s.scorer.Score(ctx, cons) + err = s.advanceNextMust(ctx, rv) + if err != nil { + return nil, err + } + break + } + + err = s.advanceNextMust(ctx, nil) + if err != nil { + return nil, err + } + } + + if rv == nil { + s.done = true + } + + return rv, nil +} + +func (s *BooleanSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + + if s.done { + return nil, nil + } + + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + + // Advance the searcher only if the cursor is trailing the lookup ID + if s.currentID == nil || s.currentID.Compare(ID) < 0 { + var err error + if s.mustSearcher != nil { + if s.currMust != nil { + ctx.DocumentMatchPool.Put(s.currMust) + } + s.currMust, err = s.mustSearcher.Advance(ctx, ID) + if err != nil { + return nil, err + } + } + + if s.shouldSearcher != nil { + if s.currShould != nil { + ctx.DocumentMatchPool.Put(s.currShould) + } + s.currShould, err = s.shouldSearcher.Advance(ctx, ID) + if err != nil { + return nil, err + } + } + + if s.mustNotSearcher != nil { + // Additional check for mustNotSearcher, whose cursor isn't tracked by + // currentID to prevent it from moving when the searcher's tracked + // position is already ahead of or at the requested ID. + if s.currMustNot == nil || s.currMustNot.IndexInternalID.Compare(ID) < 0 { + if s.currMustNot != nil { + ctx.DocumentMatchPool.Put(s.currMustNot) + } + s.currMustNot, err = s.mustNotSearcher.Advance(ctx, ID) + if err != nil { + return nil, err + } + } + } + + if s.mustSearcher != nil && s.currMust != nil { + s.currentID = s.currMust.IndexInternalID + } else if s.mustSearcher == nil && s.currShould != nil { + s.currentID = s.currShould.IndexInternalID + } else { + s.currentID = nil + } + } + + return s.Next(ctx) +} + +func (s *BooleanSearcher) Count() uint64 { + + // for now return a worst case + var sum uint64 + if s.mustSearcher != nil { + sum += s.mustSearcher.Count() + } + if s.shouldSearcher != nil { + sum += s.shouldSearcher.Count() + } + return sum +} + +func (s *BooleanSearcher) Close() error { + var err0, err1, err2 error + if s.mustSearcher != nil { + err0 = s.mustSearcher.Close() + } + if s.shouldSearcher != nil { + err1 = s.shouldSearcher.Close() + } + if s.mustNotSearcher != nil { + err2 = s.mustNotSearcher.Close() + } + if err0 != nil { + return err0 + } + if err1 != nil { + return err1 + } + if err2 != nil { + return err2 + } + return nil +} + +func (s *BooleanSearcher) Min() int { + return 0 +} + +func (s *BooleanSearcher) DocumentMatchPoolSize() int { + rv := 3 + if s.mustSearcher != nil { + rv += s.mustSearcher.DocumentMatchPoolSize() + } + if s.shouldSearcher != nil { + rv += s.shouldSearcher.DocumentMatchPoolSize() + } + if s.mustNotSearcher != nil { + rv += s.mustNotSearcher.DocumentMatchPoolSize() + } + return rv +} diff --git a/search/searcher/search_boolean_test.go b/search/searcher/search_boolean_test.go new file mode 100644 index 0000000..1c803e9 --- /dev/null +++ b/search/searcher/search_boolean_test.go @@ -0,0 +1,382 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestBooleanSearch(t *testing.T) { + if twoDocIndex == nil { + t.Fatal("its null") + } + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainTrue := search.SearcherOptions{Explain: true} + + // test 0 + beerTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustSearcher, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher}, explainTrue) + if err != nil { + t.Fatal(err) + } + martyTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + shouldSearcher, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + steveTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "steve", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustNotSearcher, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{steveTermSearcher}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, mustSearcher, shouldSearcher, mustNotSearcher, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 1 + martyTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + shouldSearcher2, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher2, dustinTermSearcher2}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + steveTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "steve", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustNotSearcher2, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{steveTermSearcher2}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher2, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, nil, shouldSearcher2, mustNotSearcher2, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 2 + steveTermSearcher3, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "steve", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustNotSearcher3, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{steveTermSearcher3}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher3, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, nil, nil, mustNotSearcher3, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 3 + beerTermSearcher4, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustSearcher4, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher4}, explainTrue) + if err != nil { + t.Fatal(err) + } + steveTermSearcher4, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "steve", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustNotSearcher4, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{steveTermSearcher4}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher4, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, mustSearcher4, nil, mustNotSearcher4, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 4 + beerTermSearcher5, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustSearcher5, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher5}, explainTrue) + if err != nil { + t.Fatal(err) + } + steveTermSearcher5, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "steve", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + martyTermSearcher5, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustNotSearcher5, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{steveTermSearcher5, martyTermSearcher5}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher5, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, mustSearcher5, nil, mustNotSearcher5, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 5 + beerTermSearcher6, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustSearcher6, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher6}, explainTrue) + if err != nil { + t.Fatal(err) + } + martyTermSearcher6, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher6, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + shouldSearcher6, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher6, dustinTermSearcher6}, 2, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher6, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, mustSearcher6, shouldSearcher6, nil, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 6 + beerTermSearcher7, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustSearcher7, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher7}, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher7, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, mustSearcher7, nil, nil, explainTrue) + if err != nil { + t.Fatal(err) + } + martyTermSearcher7, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + conjunctionSearcher7, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher7, booleanSearcher7}, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 7 + beerTermSearcher8, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustSearcher8, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher8}, explainTrue) + if err != nil { + t.Fatal(err) + } + martyTermSearcher8, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher8, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + shouldSearcher8, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher8, dustinTermSearcher8}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + steveTermSearcher8, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "steve", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + mustNotSearcher8, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{steveTermSearcher8}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + booleanSearcher8, err := NewBooleanSearcher(context.TODO(), twoDocIndexReader, mustSearcher8, shouldSearcher8, mustNotSearcher8, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher8a, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + conjunctionSearcher8, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{booleanSearcher8, dustinTermSearcher8a}, explainTrue) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + searcher search.Searcher + results []*search.DocumentMatch + }{ + { + searcher: booleanSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 0.9818005051949021, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.808709699395535, + }, + { + IndexInternalID: index.IndexInternalID("4"), + Score: 0.34618161159873423, + }, + }, + }, + { + searcher: booleanSearcher2, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 0.6775110856165737, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.6775110856165737, + }, + }, + }, + // no MUST or SHOULD clauses yields no results + { + searcher: booleanSearcher3, + results: []*search.DocumentMatch{}, + }, + { + searcher: booleanSearcher4, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.5, + }, + { + IndexInternalID: index.IndexInternalID("4"), + Score: 1.0, + }, + }, + }, + { + searcher: booleanSearcher5, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.5, + }, + { + IndexInternalID: index.IndexInternalID("4"), + Score: 1.0, + }, + }, + }, + { + searcher: booleanSearcher6, + results: []*search.DocumentMatch{}, + }, + // test a conjunction query with a nested boolean + { + searcher: conjunctionSearcher7, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 2.0097428702814377, + }, + }, + }, + { + searcher: conjunctionSearcher8, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("3"), + Score: 2.0681575785068107, + }, + }, + }, + } + + for testIndex, test := range tests { + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(test.searcher.DocumentMatchPoolSize(), 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + if i < len(test.results) { + if !next.IndexInternalID.Equals(test.results[i].IndexInternalID) { + t.Errorf("expected result %d to have id %s got %s for test %d", i, test.results[i].IndexInternalID, next.IndexInternalID, testIndex) + } + if !scoresCloseEnough(next.Score, test.results[i].Score) { + t.Errorf("expected result %d to have score %v got %v for test %d", i, test.results[i].Score, next.Score, testIndex) + t.Logf("scoring explanation: %s", next.Expl) + } + } + ctx.DocumentMatchPool.Put(next) + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.results) != i { + t.Errorf("expected %d results got %d for test %d", len(test.results), i, testIndex) + } + } +} diff --git a/search/searcher/search_conjunction.go b/search/searcher/search_conjunction.go new file mode 100644 index 0000000..57d8855 --- /dev/null +++ b/search/searcher/search_conjunction.go @@ -0,0 +1,285 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "math" + "reflect" + "sort" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeConjunctionSearcher int + +func init() { + var cs ConjunctionSearcher + reflectStaticSizeConjunctionSearcher = int(reflect.TypeOf(cs).Size()) +} + +type ConjunctionSearcher struct { + indexReader index.IndexReader + searchers []search.Searcher + queryNorm float64 + currs []*search.DocumentMatch + maxIDIdx int + scorer *scorer.ConjunctionQueryScorer + initialized bool + options search.SearcherOptions + bytesRead uint64 +} + +func NewConjunctionSearcher(ctx context.Context, indexReader index.IndexReader, + qsearchers []search.Searcher, options search.SearcherOptions) ( + search.Searcher, error, +) { + // build the sorted downstream searchers + searchers := make(OrderedSearcherList, len(qsearchers)) + copy(searchers, qsearchers) + sort.Sort(searchers) + + // attempt the "unadorned" conjunction optimization only when we + // do not need extra information like freq-norm's or term vectors + if len(searchers) > 1 && + options.Score == "none" && !options.IncludeTermVectors { + rv, err := optimizeCompositeSearcher(ctx, "conjunction:unadorned", + indexReader, searchers, options) + if err != nil || rv != nil { + return rv, err + } + } + + // build our searcher + rv := ConjunctionSearcher{ + indexReader: indexReader, + options: options, + searchers: searchers, + currs: make([]*search.DocumentMatch, len(searchers)), + scorer: scorer.NewConjunctionQueryScorer(options), + } + rv.computeQueryNorm() + + // attempt push-down conjunction optimization when there's >1 searchers + if len(searchers) > 1 { + rv, err := optimizeCompositeSearcher(ctx, "conjunction", + indexReader, searchers, options) + if err != nil || rv != nil { + return rv, err + } + } + + return &rv, nil +} + +func (s *ConjunctionSearcher) computeQueryNorm() { + // first calculate sum of squared weights + sumOfSquaredWeights := 0.0 + for _, searcher := range s.searchers { + sumOfSquaredWeights += searcher.Weight() + } + // now compute query norm from this + s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights) + // finally tell all the downstream searchers the norm + for _, searcher := range s.searchers { + searcher.SetQueryNorm(s.queryNorm) + } +} + +func (s *ConjunctionSearcher) Size() int { + sizeInBytes := reflectStaticSizeConjunctionSearcher + size.SizeOfPtr + + s.scorer.Size() + + for _, entry := range s.searchers { + sizeInBytes += entry.Size() + } + + for _, entry := range s.currs { + if entry != nil { + sizeInBytes += entry.Size() + } + } + + return sizeInBytes +} + +func (s *ConjunctionSearcher) initSearchers(ctx *search.SearchContext) error { + var err error + // get all searchers pointing at their first match + for i, searcher := range s.searchers { + if s.currs[i] != nil { + ctx.DocumentMatchPool.Put(s.currs[i]) + } + s.currs[i], err = searcher.Next(ctx) + if err != nil { + return err + } + } + s.initialized = true + return nil +} + +func (s *ConjunctionSearcher) Weight() float64 { + var rv float64 + for _, searcher := range s.searchers { + rv += searcher.Weight() + } + return rv +} + +func (s *ConjunctionSearcher) SetQueryNorm(qnorm float64) { + for _, searcher := range s.searchers { + searcher.SetQueryNorm(qnorm) + } +} + +func (s *ConjunctionSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + var rv *search.DocumentMatch + var err error +OUTER: + for s.maxIDIdx < len(s.currs) && s.currs[s.maxIDIdx] != nil { + maxID := s.currs[s.maxIDIdx].IndexInternalID + + i := 0 + for i < len(s.currs) { + if s.currs[i] == nil { + return nil, nil + } + + if i == s.maxIDIdx { + i++ + continue + } + + cmp := maxID.Compare(s.currs[i].IndexInternalID) + if cmp == 0 { + i++ + continue + } + + if cmp < 0 { + // maxID < currs[i], so we found a new maxIDIdx + s.maxIDIdx = i + + // advance the positions where [0 <= x < i], since we + // know they were equal to the former max entry + maxID = s.currs[s.maxIDIdx].IndexInternalID + for x := 0; x < i; x++ { + err = s.advanceChild(ctx, x, maxID) + if err != nil { + return nil, err + } + } + + continue OUTER + } + + // maxID > currs[i], so need to advance searchers[i] + err = s.advanceChild(ctx, i, maxID) + if err != nil { + return nil, err + } + + // don't bump i, so that we'll examine the just-advanced + // currs[i] again + } + + // if we get here, a doc matched all readers, so score and add it + rv = s.scorer.Score(ctx, s.currs) + + // we know all the searchers are pointing at the same thing + // so they all need to be bumped + for i, searcher := range s.searchers { + if s.currs[i] != rv { + ctx.DocumentMatchPool.Put(s.currs[i]) + } + s.currs[i], err = searcher.Next(ctx) + if err != nil { + return nil, err + } + } + + // don't continue now, wait for the next call to Next() + break + } + return rv, nil +} + +func (s *ConjunctionSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + for i := range s.searchers { + if s.currs[i] != nil && s.currs[i].IndexInternalID.Compare(ID) >= 0 { + continue + } + err := s.advanceChild(ctx, i, ID) + if err != nil { + return nil, err + } + } + return s.Next(ctx) +} + +func (s *ConjunctionSearcher) advanceChild(ctx *search.SearchContext, i int, ID index.IndexInternalID) (err error) { + if s.currs[i] != nil { + ctx.DocumentMatchPool.Put(s.currs[i]) + } + s.currs[i], err = s.searchers[i].Advance(ctx, ID) + return err +} + +func (s *ConjunctionSearcher) Count() uint64 { + // for now return a worst case + var sum uint64 + for _, searcher := range s.searchers { + sum += searcher.Count() + } + return sum +} + +func (s *ConjunctionSearcher) Close() (rv error) { + for _, searcher := range s.searchers { + err := searcher.Close() + if err != nil && rv == nil { + rv = err + } + } + return rv +} + +func (s *ConjunctionSearcher) Min() int { + return 0 +} + +func (s *ConjunctionSearcher) DocumentMatchPoolSize() int { + rv := len(s.currs) + for _, s := range s.searchers { + rv += s.DocumentMatchPoolSize() + } + return rv +} diff --git a/search/searcher/search_conjunction_test.go b/search/searcher/search_conjunction_test.go new file mode 100644 index 0000000..27d2405 --- /dev/null +++ b/search/searcher/search_conjunction_test.go @@ -0,0 +1,438 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestConjunctionSearch(t *testing.T) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainTrue := search.SearcherOptions{Explain: true} + + // test 0 + beerTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + martyTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + beerAndMartySearcher, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher, martyTermSearcher}, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 1 + angstTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "angst", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + beerTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + angstAndBeerSearcher, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{angstTermSearcher, beerTermSearcher2}, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 2 + beerTermSearcher3, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + jackTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "jack", "name", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + beerAndJackSearcher, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher3, jackTermSearcher}, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 3 + beerTermSearcher4, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + misterTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "mister", "title", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + beerAndMisterSearcher, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher4, misterTermSearcher}, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 4 + couchbaseTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "couchbase", "street", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + misterTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "mister", "title", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + couchbaseAndMisterSearcher, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{couchbaseTermSearcher, misterTermSearcher2}, explainTrue) + if err != nil { + t.Fatal(err) + } + + // test 5 + beerTermSearcher5, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "beer", "desc", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + couchbaseTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "couchbase", "street", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + misterTermSearcher3, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "mister", "title", 5.0, explainTrue) + if err != nil { + t.Fatal(err) + } + couchbaseAndMisterSearcher2, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{couchbaseTermSearcher2, misterTermSearcher3}, explainTrue) + if err != nil { + t.Fatal(err) + } + beerAndCouchbaseAndMisterSearcher, err := NewConjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{beerTermSearcher5, couchbaseAndMisterSearcher2}, explainTrue) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + searcher search.Searcher + results []*search.DocumentMatch + }{ + { + searcher: beerAndMartySearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 2.0097428702814377, + }, + }, + }, + { + searcher: angstAndBeerSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("2"), + Score: 1.0807601687084403, + }, + }, + }, + { + searcher: beerAndJackSearcher, + results: []*search.DocumentMatch{}, + }, + { + searcher: beerAndMisterSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("2"), + Score: 1.2877980334016337, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 1.2877980334016337, + }, + }, + }, + { + searcher: couchbaseAndMisterSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("2"), + Score: 1.4436599157093672, + }, + }, + }, + { + searcher: beerAndCouchbaseAndMisterSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("2"), + Score: 1.441614953806971, + }, + }, + }, + } + + for testIndex, test := range tests { + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(10, 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + if i < len(test.results) { + if !next.IndexInternalID.Equals(test.results[i].IndexInternalID) { + t.Errorf("expected result %d to have id %s got %s for test %d", i, test.results[i].IndexInternalID, next.IndexInternalID, testIndex) + } + if !scoresCloseEnough(next.Score, test.results[i].Score) { + t.Errorf("expected result %d to have score %v got %v for test %d", i, test.results[i].Score, next.Score, testIndex) + t.Logf("scoring explanation: %s", next.Expl) + } + } + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.results) != i { + t.Errorf("expected %d results got %d for test %d", len(test.results), i, testIndex) + } + } +} + +type compositeSearchOptimizationTest struct { + fieldTerms []string + expectEmpty string +} + +func TestScorchCompositeSearchOptimizations(t *testing.T) { + dir, _ := os.MkdirTemp("", "scorchTwoDoc") + defer func() { + _ = os.RemoveAll(dir) + }() + + twoDocIndex := initTwoDocScorch(dir) + + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + tests := []compositeSearchOptimizationTest{ + { + fieldTerms: []string{}, + expectEmpty: "conjunction,disjunction", + }, + { + fieldTerms: []string{"name:marty"}, + expectEmpty: "", + }, + { + fieldTerms: []string{"name:marty", "desc:beer"}, + expectEmpty: "", + }, + { + fieldTerms: []string{"name:marty", "name:marty"}, + expectEmpty: "", + }, + { + fieldTerms: []string{"name:marty", "desc:beer", "title:mister", "street:couchbase"}, + expectEmpty: "conjunction", + }, + { + fieldTerms: []string{"name:steve", "desc:beer", "title:mister", "street:couchbase"}, + expectEmpty: "", + }, + + { + fieldTerms: []string{"name:NotARealName"}, + expectEmpty: "conjunction,disjunction", + }, + { + fieldTerms: []string{"name:NotARealName", "name:marty"}, + expectEmpty: "conjunction", + }, + { + fieldTerms: []string{"name:NotARealName", "name:marty", "desc:beer"}, + expectEmpty: "conjunction", + }, + { + fieldTerms: []string{"name:NotARealName", "name:marty", "name:marty"}, + expectEmpty: "conjunction", + }, + { + fieldTerms: []string{"name:NotARealName", "name:marty", "desc:beer", "title:mister", "street:couchbase"}, + expectEmpty: "conjunction", + }, + } + + // The theme of this unit test is that given one of the above + // search test cases -- no matter what searcher options we + // provide, across either conjunctions or disjunctions, whether we + // have optimizations that are enabled or disabled, the set of doc + // ID's from the search results from any of those combinations + // should be the same. + searcherOptionsToCompare := []search.SearcherOptions{ + {}, + {Explain: true}, + {IncludeTermVectors: true}, + {IncludeTermVectors: true, Explain: true}, + {Score: "none"}, + {Score: "none", IncludeTermVectors: true}, + {Score: "none", IncludeTermVectors: true, Explain: true}, + {Score: "none", Explain: true}, + } + + testScorchCompositeSearchOptimizations(t, twoDocIndexReader, tests, + searcherOptionsToCompare, "conjunction") + + testScorchCompositeSearchOptimizations(t, twoDocIndexReader, tests, + searcherOptionsToCompare, "disjunction") +} + +func testScorchCompositeSearchOptimizations(t *testing.T, indexReader index.IndexReader, + tests []compositeSearchOptimizationTest, + searcherOptionsToCompare []search.SearcherOptions, + compositeKind string, +) { + for testi := range tests { + resultsToCompare := map[string]bool{} + + testScorchCompositeSearchOptimizationsHelper(t, indexReader, tests, testi, + searcherOptionsToCompare, compositeKind, false, resultsToCompare) + + testScorchCompositeSearchOptimizationsHelper(t, indexReader, tests, testi, + searcherOptionsToCompare, compositeKind, true, resultsToCompare) + } +} + +func testScorchCompositeSearchOptimizationsHelper( + t *testing.T, indexReader index.IndexReader, + tests []compositeSearchOptimizationTest, testi int, + searcherOptionsToCompare []search.SearcherOptions, + compositeKind string, allowOptimizations bool, resultsToCompare map[string]bool, +) { + // Save the global allowed optimization settings to restore later. + optimizeConjunction := scorch.OptimizeConjunction + optimizeConjunctionUnadorned := scorch.OptimizeConjunctionUnadorned + optimizeDisjunctionUnadorned := scorch.OptimizeDisjunctionUnadorned + optimizeDisjunctionUnadornedMinChildCardinality := scorch.OptimizeDisjunctionUnadornedMinChildCardinality + + scorch.OptimizeConjunction = allowOptimizations + scorch.OptimizeConjunctionUnadorned = allowOptimizations + scorch.OptimizeDisjunctionUnadorned = allowOptimizations + + if allowOptimizations { + scorch.OptimizeDisjunctionUnadornedMinChildCardinality = uint64(0) + } + + defer func() { + scorch.OptimizeConjunction = optimizeConjunction + scorch.OptimizeConjunctionUnadorned = optimizeConjunctionUnadorned + scorch.OptimizeDisjunctionUnadorned = optimizeDisjunctionUnadorned + scorch.OptimizeDisjunctionUnadornedMinChildCardinality = optimizeDisjunctionUnadornedMinChildCardinality + }() + + test := tests[testi] + + for searcherOptionsI, searcherOptions := range searcherOptionsToCompare { + // Construct the leaf term searchers. + var searchers []search.Searcher + + for _, fieldTerm := range test.fieldTerms { + ft := strings.Split(fieldTerm, ":") + field := ft[0] + term := ft[1] + + searcher, err := NewTermSearcher(context.TODO(), indexReader, term, field, 1.0, searcherOptions) + if err != nil { + t.Fatal(err) + } + + searchers = append(searchers, searcher) + } + + // Construct the composite searcher. + var cs search.Searcher + var err error + if compositeKind == "conjunction" { + cs, err = NewConjunctionSearcher(context.TODO(), indexReader, searchers, searcherOptions) + } else { + cs, err = NewDisjunctionSearcher(context.TODO(), indexReader, searchers, 0, searcherOptions) + } + if err != nil { + t.Fatal(err) + } + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(10, 0), + } + + next, err := cs.Next(ctx) + i := 0 + for err == nil && next != nil { + docID, err := indexReader.ExternalID(next.IndexInternalID) + if err != nil { + t.Fatal(err) + } + + if searcherOptionsI == 0 && allowOptimizations == false { + resultsToCompare[string(docID)] = true + } else { + if !resultsToCompare[string(docID)] { + t.Errorf("missing %s", string(docID)) + } + } + + next, err = cs.Next(ctx) + if err != nil { + t.Fatalf("error iterating searcher: %v", err) + } + + i++ + } + + if i != len(resultsToCompare) { + t.Errorf("mismatched count, %d vs %d", i, len(resultsToCompare)) + } + + if i == 0 && !strings.Contains(test.expectEmpty, compositeKind) { + t.Errorf("testi: %d, compositeKind: %s, allowOptimizations: %t,"+ + " searcherOptionsI: %d, searcherOptions: %#v,"+ + " expected some results but got no results on test: %#v", + testi, compositeKind, allowOptimizations, + searcherOptionsI, searcherOptions, test) + } + } +} diff --git a/search/searcher/search_disjunction.go b/search/searcher/search_disjunction.go new file mode 100644 index 0000000..434c705 --- /dev/null +++ b/search/searcher/search_disjunction.go @@ -0,0 +1,131 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +// DisjunctionMaxClauseCount is a compile time setting that applications can +// adjust to non-zero value to cause the DisjunctionSearcher to return an +// error instead of exeucting searches when the size exceeds this value. +var DisjunctionMaxClauseCount = 0 + +// DisjunctionHeapTakeover is a compile time setting that applications can +// adjust to control when the DisjunctionSearcher will switch from a simple +// slice implementation to a heap implementation. +var DisjunctionHeapTakeover = 10 + +func NewDisjunctionSearcher(ctx context.Context, indexReader index.IndexReader, + qsearchers []search.Searcher, min float64, options search.SearcherOptions) ( + search.Searcher, error) { + return newDisjunctionSearcher(ctx, indexReader, qsearchers, min, options, true) +} + +func optionsDisjunctionOptimizable(options search.SearcherOptions) bool { + rv := options.Score == "none" && !options.IncludeTermVectors + return rv +} + +func newDisjunctionSearcher(ctx context.Context, indexReader index.IndexReader, + qsearchers []search.Searcher, min float64, options search.SearcherOptions, + limit bool) (search.Searcher, error) { + + var disjOverKNN bool + if ctx != nil { + disjOverKNN, _ = ctx.Value(search.IncludeScoreBreakdownKey).(bool) + } + if disjOverKNN { + // The KNN Searcher optimization is a necessary pre-req for the KNN Searchers, + // not an optional optimization like for, say term searchers. + // It's an optimization to repeat search an open vector index when applicable, + // rather than individually opening and searching a vector index. + err := optimizeKNN(ctx, indexReader, qsearchers) + if err != nil { + return nil, err + } + } else { + // attempt the "unadorned" disjunction optimization only when we + // do not need extra information like freq-norm's or term vectors + // and the requested min is simple + if len(qsearchers) > 1 && min <= 1 && + optionsDisjunctionOptimizable(options) { + rv, err := optimizeCompositeSearcher(ctx, "disjunction:unadorned", + indexReader, qsearchers, options) + if err != nil || rv != nil { + return rv, err + } + } + } + + if len(qsearchers) > DisjunctionHeapTakeover { + return newDisjunctionHeapSearcher(ctx, indexReader, qsearchers, min, options, + limit) + } + return newDisjunctionSliceSearcher(ctx, indexReader, qsearchers, min, options, + limit) +} + +func optimizeCompositeSearcher(ctx context.Context, optimizationKind string, + indexReader index.IndexReader, qsearchers []search.Searcher, + options search.SearcherOptions) (search.Searcher, error) { + var octx index.OptimizableContext + + for _, searcher := range qsearchers { + o, ok := searcher.(index.Optimizable) + if !ok { + return nil, nil + } + + var err error + octx, err = o.Optimize(optimizationKind, octx) + if err != nil { + return nil, err + } + + if octx == nil { + return nil, nil + } + } + + optimized, err := octx.Finish() + if err != nil || optimized == nil { + return nil, err + } + + tfr, ok := optimized.(index.TermFieldReader) + if !ok { + return nil, nil + } + + return newTermSearcherFromReader(ctx, indexReader, tfr, + []byte(optimizationKind), "*", 1.0, options) +} + +func tooManyClauses(count int) bool { + if DisjunctionMaxClauseCount != 0 && count > DisjunctionMaxClauseCount { + return true + } + return false +} + +func tooManyClausesErr(field string, count int) error { + return fmt.Errorf("TooManyClauses over field: `%s` [%d > maxClauseCount,"+ + " which is set to %d]", field, count, DisjunctionMaxClauseCount) +} diff --git a/search/searcher/search_disjunction_heap.go b/search/searcher/search_disjunction_heap.go new file mode 100644 index 0000000..3da876b --- /dev/null +++ b/search/searcher/search_disjunction_heap.go @@ -0,0 +1,367 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "bytes" + "container/heap" + "context" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeDisjunctionHeapSearcher int +var reflectStaticSizeSearcherCurr int + +func init() { + var dhs DisjunctionHeapSearcher + reflectStaticSizeDisjunctionHeapSearcher = int(reflect.TypeOf(dhs).Size()) + + var sc SearcherCurr + reflectStaticSizeSearcherCurr = int(reflect.TypeOf(sc).Size()) +} + +type SearcherCurr struct { + searcher search.Searcher + curr *search.DocumentMatch + matchingIdx int +} + +type DisjunctionHeapSearcher struct { + indexReader index.IndexReader + + numSearchers int + scorer *scorer.DisjunctionQueryScorer + min int + queryNorm float64 + retrieveScoreBreakdown bool + initialized bool + searchers []search.Searcher + heap []*SearcherCurr + + matching []*search.DocumentMatch + matchingIdxs []int + matchingCurrs []*SearcherCurr + + bytesRead uint64 +} + +func newDisjunctionHeapSearcher(ctx context.Context, indexReader index.IndexReader, + searchers []search.Searcher, min float64, options search.SearcherOptions, + limit bool) ( + *DisjunctionHeapSearcher, error) { + if limit && tooManyClauses(len(searchers)) { + return nil, tooManyClausesErr("", len(searchers)) + } + var retrieveScoreBreakdown bool + if ctx != nil { + retrieveScoreBreakdown, _ = ctx.Value(search.IncludeScoreBreakdownKey).(bool) + } + + // build our searcher + rv := DisjunctionHeapSearcher{ + indexReader: indexReader, + searchers: searchers, + numSearchers: len(searchers), + scorer: scorer.NewDisjunctionQueryScorer(options), + min: int(min), + matching: make([]*search.DocumentMatch, len(searchers)), + matchingCurrs: make([]*SearcherCurr, len(searchers)), + matchingIdxs: make([]int, len(searchers)), + retrieveScoreBreakdown: retrieveScoreBreakdown, + heap: make([]*SearcherCurr, 0, len(searchers)), + } + rv.computeQueryNorm() + return &rv, nil +} + +func (s *DisjunctionHeapSearcher) computeQueryNorm() { + // first calculate sum of squared weights + sumOfSquaredWeights := 0.0 + for _, searcher := range s.searchers { + sumOfSquaredWeights += searcher.Weight() + } + // now compute query norm from this + s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights) + // finally tell all the downstream searchers the norm + for _, searcher := range s.searchers { + searcher.SetQueryNorm(s.queryNorm) + } +} + +func (s *DisjunctionHeapSearcher) Size() int { + sizeInBytes := reflectStaticSizeDisjunctionHeapSearcher + size.SizeOfPtr + + s.scorer.Size() + + for _, entry := range s.searchers { + sizeInBytes += entry.Size() + } + + for _, entry := range s.matching { + if entry != nil { + sizeInBytes += entry.Size() + } + } + + // for matchingCurrs and heap, just use static size * len + // since searchers and document matches already counted above + sizeInBytes += len(s.matchingCurrs) * reflectStaticSizeSearcherCurr + sizeInBytes += len(s.heap) * reflectStaticSizeSearcherCurr + sizeInBytes += len(s.matchingIdxs) * size.SizeOfInt + + return sizeInBytes +} + +func (s *DisjunctionHeapSearcher) initSearchers(ctx *search.SearchContext) error { + // alloc a single block of SearcherCurrs + block := make([]SearcherCurr, len(s.searchers)) + + // get all searchers pointing at their first match + for i, searcher := range s.searchers { + curr, err := searcher.Next(ctx) + if err != nil { + return err + } + if curr != nil { + block[i].searcher = searcher + block[i].curr = curr + block[i].matchingIdx = i + heap.Push(s, &block[i]) + } + } + + err := s.updateMatches() + if err != nil { + return err + } + s.initialized = true + return nil +} + +func (s *DisjunctionHeapSearcher) updateMatches() error { + matching := s.matching[:0] + matchingCurrs := s.matchingCurrs[:0] + matchingIdxs := s.matchingIdxs[:0] + + if len(s.heap) > 0 { + + // top of the heap is our next hit + next := heap.Pop(s).(*SearcherCurr) + matching = append(matching, next.curr) + matchingCurrs = append(matchingCurrs, next) + matchingIdxs = append(matchingIdxs, next.matchingIdx) + + // now as long as top of heap matches, keep popping + for len(s.heap) > 0 && bytes.Compare(next.curr.IndexInternalID, s.heap[0].curr.IndexInternalID) == 0 { + next = heap.Pop(s).(*SearcherCurr) + matching = append(matching, next.curr) + matchingCurrs = append(matchingCurrs, next) + matchingIdxs = append(matchingIdxs, next.matchingIdx) + } + } + + s.matching = matching + s.matchingCurrs = matchingCurrs + s.matchingIdxs = matchingIdxs + + return nil +} + +func (s *DisjunctionHeapSearcher) Weight() float64 { + var rv float64 + for _, searcher := range s.searchers { + rv += searcher.Weight() + } + return rv +} + +func (s *DisjunctionHeapSearcher) SetQueryNorm(qnorm float64) { + for _, searcher := range s.searchers { + searcher.SetQueryNorm(qnorm) + } +} + +func (s *DisjunctionHeapSearcher) Next(ctx *search.SearchContext) ( + *search.DocumentMatch, error) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + + var rv *search.DocumentMatch + found := false + for !found && len(s.matching) > 0 { + if len(s.matching) >= s.min { + found = true + if s.retrieveScoreBreakdown { + // just return score and expl breakdown here, since it is a disjunction over knn searchers, + // and the final score and expl is calculated in the knn collector + rv = s.scorer.ScoreAndExplBreakdown(ctx, s.matching, s.matchingIdxs, nil, s.numSearchers) + } else { + // score this match + rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers) + } + } + + // invoke next on all the matching searchers + for _, matchingCurr := range s.matchingCurrs { + if matchingCurr.curr != rv { + ctx.DocumentMatchPool.Put(matchingCurr.curr) + } + curr, err := matchingCurr.searcher.Next(ctx) + if err != nil { + return nil, err + } + if curr != nil { + matchingCurr.curr = curr + heap.Push(s, matchingCurr) + } + } + + err := s.updateMatches() + if err != nil { + return nil, err + } + } + + return rv, nil +} + +func (s *DisjunctionHeapSearcher) Advance(ctx *search.SearchContext, + ID index.IndexInternalID) (*search.DocumentMatch, error) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + + // if there is anything in matching, toss it back onto the heap + for _, matchingCurr := range s.matchingCurrs { + heap.Push(s, matchingCurr) + } + s.matching = s.matching[:0] + s.matchingCurrs = s.matchingCurrs[:0] + + // find all searchers that actually need to be advanced + // advance them, using s.matchingCurrs as temp storage + for len(s.heap) > 0 && bytes.Compare(s.heap[0].curr.IndexInternalID, ID) < 0 { + searcherCurr := heap.Pop(s).(*SearcherCurr) + ctx.DocumentMatchPool.Put(searcherCurr.curr) + curr, err := searcherCurr.searcher.Advance(ctx, ID) + if err != nil { + return nil, err + } + if curr != nil { + searcherCurr.curr = curr + s.matchingCurrs = append(s.matchingCurrs, searcherCurr) + } + } + // now all of the searchers that we advanced have to be pushed back + for _, matchingCurr := range s.matchingCurrs { + heap.Push(s, matchingCurr) + } + // reset our temp space + s.matchingCurrs = s.matchingCurrs[:0] + + err := s.updateMatches() + if err != nil { + return nil, err + } + + return s.Next(ctx) +} + +func (s *DisjunctionHeapSearcher) Count() uint64 { + // for now return a worst case + var sum uint64 + for _, searcher := range s.searchers { + sum += searcher.Count() + } + return sum +} + +func (s *DisjunctionHeapSearcher) Close() (rv error) { + for _, searcher := range s.searchers { + err := searcher.Close() + if err != nil && rv == nil { + rv = err + } + } + return rv +} + +func (s *DisjunctionHeapSearcher) Min() int { + return s.min +} + +func (s *DisjunctionHeapSearcher) DocumentMatchPoolSize() int { + rv := len(s.searchers) + for _, s := range s.searchers { + rv += s.DocumentMatchPoolSize() + } + return rv +} + +// a disjunction searcher implements the index.Optimizable interface +// but only activates on an edge case where the disjunction is a +// wrapper around a single Optimizable child searcher +func (s *DisjunctionHeapSearcher) Optimize(kind string, octx index.OptimizableContext) ( + index.OptimizableContext, error) { + if len(s.searchers) == 1 { + o, ok := s.searchers[0].(index.Optimizable) + if ok { + return o.Optimize(kind, octx) + } + } + + return nil, nil +} + +// heap impl + +func (s *DisjunctionHeapSearcher) Len() int { return len(s.heap) } + +func (s *DisjunctionHeapSearcher) Less(i, j int) bool { + if s.heap[i].curr == nil { + return true + } else if s.heap[j].curr == nil { + return false + } + return bytes.Compare(s.heap[i].curr.IndexInternalID, s.heap[j].curr.IndexInternalID) < 0 +} + +func (s *DisjunctionHeapSearcher) Swap(i, j int) { + s.heap[i], s.heap[j] = s.heap[j], s.heap[i] +} + +func (s *DisjunctionHeapSearcher) Push(x interface{}) { + s.heap = append(s.heap, x.(*SearcherCurr)) +} + +func (s *DisjunctionHeapSearcher) Pop() interface{} { + old := s.heap + n := len(old) + x := old[n-1] + s.heap = old[0 : n-1] + return x +} diff --git a/search/searcher/search_disjunction_slice.go b/search/searcher/search_disjunction_slice.go new file mode 100644 index 0000000..6a92ffa --- /dev/null +++ b/search/searcher/search_disjunction_slice.go @@ -0,0 +1,334 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "math" + "reflect" + "sort" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeDisjunctionSliceSearcher int + +func init() { + var ds DisjunctionSliceSearcher + reflectStaticSizeDisjunctionSliceSearcher = int(reflect.TypeOf(ds).Size()) +} + +type DisjunctionSliceSearcher struct { + indexReader index.IndexReader + searchers []search.Searcher + originalPos []int + numSearchers int + queryNorm float64 + retrieveScoreBreakdown bool + currs []*search.DocumentMatch + scorer *scorer.DisjunctionQueryScorer + min int + matching []*search.DocumentMatch + matchingIdxs []int + initialized bool + bytesRead uint64 +} + +func newDisjunctionSliceSearcher(ctx context.Context, indexReader index.IndexReader, + qsearchers []search.Searcher, min float64, options search.SearcherOptions, + limit bool) ( + *DisjunctionSliceSearcher, error, +) { + if limit && tooManyClauses(len(qsearchers)) { + return nil, tooManyClausesErr("", len(qsearchers)) + } + + var searchers OrderedSearcherList + var originalPos []int + var retrieveScoreBreakdown bool + if ctx != nil { + retrieveScoreBreakdown, _ = ctx.Value(search.IncludeScoreBreakdownKey).(bool) + } + + if retrieveScoreBreakdown { + // needed only when kNN is in picture + sortedSearchers := &OrderedPositionalSearcherList{ + searchers: make([]search.Searcher, len(qsearchers)), + index: make([]int, len(qsearchers)), + } + for i, searcher := range qsearchers { + sortedSearchers.searchers[i] = searcher + sortedSearchers.index[i] = i + } + sort.Sort(sortedSearchers) + searchers = sortedSearchers.searchers + originalPos = sortedSearchers.index + } else { + searchers = make(OrderedSearcherList, len(qsearchers)) + copy(searchers, qsearchers) + sort.Sort(searchers) + } + + rv := DisjunctionSliceSearcher{ + indexReader: indexReader, + searchers: searchers, + originalPos: originalPos, + numSearchers: len(searchers), + currs: make([]*search.DocumentMatch, len(searchers)), + scorer: scorer.NewDisjunctionQueryScorer(options), + min: int(min), + retrieveScoreBreakdown: retrieveScoreBreakdown, + + matching: make([]*search.DocumentMatch, len(searchers)), + matchingIdxs: make([]int, len(searchers)), + } + rv.computeQueryNorm() + return &rv, nil +} + +func (s *DisjunctionSliceSearcher) computeQueryNorm() { + // first calculate sum of squared weights + sumOfSquaredWeights := 0.0 + for _, searcher := range s.searchers { + sumOfSquaredWeights += searcher.Weight() + } + // now compute query norm from this + s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights) + // finally tell all the downstream searchers the norm + for _, searcher := range s.searchers { + searcher.SetQueryNorm(s.queryNorm) + } +} + +func (s *DisjunctionSliceSearcher) Size() int { + sizeInBytes := reflectStaticSizeDisjunctionSliceSearcher + size.SizeOfPtr + + s.scorer.Size() + + for _, entry := range s.searchers { + sizeInBytes += entry.Size() + } + + for _, entry := range s.currs { + if entry != nil { + sizeInBytes += entry.Size() + } + } + + for _, entry := range s.matching { + if entry != nil { + sizeInBytes += entry.Size() + } + } + + sizeInBytes += len(s.matchingIdxs) * size.SizeOfInt + sizeInBytes += len(s.originalPos) * size.SizeOfInt + + return sizeInBytes +} + +func (s *DisjunctionSliceSearcher) initSearchers(ctx *search.SearchContext) error { + var err error + // get all searchers pointing at their first match + for i, searcher := range s.searchers { + if s.currs[i] != nil { + ctx.DocumentMatchPool.Put(s.currs[i]) + } + s.currs[i], err = searcher.Next(ctx) + if err != nil { + return err + } + } + + err = s.updateMatches() + if err != nil { + return err + } + + s.initialized = true + return nil +} + +func (s *DisjunctionSliceSearcher) updateMatches() error { + matching := s.matching[:0] + matchingIdxs := s.matchingIdxs[:0] + + for i := 0; i < len(s.currs); i++ { + curr := s.currs[i] + if curr == nil { + continue + } + + if len(matching) > 0 { + cmp := curr.IndexInternalID.Compare(matching[0].IndexInternalID) + if cmp > 0 { + continue + } + + if cmp < 0 { + matching = matching[:0] + matchingIdxs = matchingIdxs[:0] + } + } + matching = append(matching, curr) + matchingIdxs = append(matchingIdxs, i) + } + + s.matching = matching + s.matchingIdxs = matchingIdxs + + return nil +} + +func (s *DisjunctionSliceSearcher) Weight() float64 { + var rv float64 + for _, searcher := range s.searchers { + rv += searcher.Weight() + } + return rv +} + +func (s *DisjunctionSliceSearcher) SetQueryNorm(qnorm float64) { + for _, searcher := range s.searchers { + searcher.SetQueryNorm(qnorm) + } +} + +func (s *DisjunctionSliceSearcher) Next(ctx *search.SearchContext) ( + *search.DocumentMatch, error, +) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + var err error + var rv *search.DocumentMatch + + found := false + for !found && len(s.matching) > 0 { + if len(s.matching) >= s.min { + found = true + if s.retrieveScoreBreakdown { + // just return score and expl breakdown here, since it is a disjunction over knn searchers, + // and the final score and expl is calculated in the knn collector + rv = s.scorer.ScoreAndExplBreakdown(ctx, s.matching, s.matchingIdxs, s.originalPos, s.numSearchers) + } else { + // score this match + rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers) + } + } + + // invoke next on all the matching searchers + for _, i := range s.matchingIdxs { + searcher := s.searchers[i] + if s.currs[i] != rv { + ctx.DocumentMatchPool.Put(s.currs[i]) + } + s.currs[i], err = searcher.Next(ctx) + if err != nil { + return nil, err + } + } + + err = s.updateMatches() + if err != nil { + return nil, err + } + } + return rv, nil +} + +func (s *DisjunctionSliceSearcher) Advance(ctx *search.SearchContext, + ID index.IndexInternalID, +) (*search.DocumentMatch, error) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + // get all searchers pointing at their first match + var err error + for i, searcher := range s.searchers { + if s.currs[i] != nil { + if s.currs[i].IndexInternalID.Compare(ID) >= 0 { + continue + } + ctx.DocumentMatchPool.Put(s.currs[i]) + } + s.currs[i], err = searcher.Advance(ctx, ID) + if err != nil { + return nil, err + } + } + + err = s.updateMatches() + if err != nil { + return nil, err + } + + return s.Next(ctx) +} + +func (s *DisjunctionSliceSearcher) Count() uint64 { + // for now return a worst case + var sum uint64 + for _, searcher := range s.searchers { + sum += searcher.Count() + } + return sum +} + +func (s *DisjunctionSliceSearcher) Close() (rv error) { + for _, searcher := range s.searchers { + err := searcher.Close() + if err != nil && rv == nil { + rv = err + } + } + return rv +} + +func (s *DisjunctionSliceSearcher) Min() int { + return s.min +} + +func (s *DisjunctionSliceSearcher) DocumentMatchPoolSize() int { + rv := len(s.currs) + for _, s := range s.searchers { + rv += s.DocumentMatchPoolSize() + } + return rv +} + +// a disjunction searcher implements the index.Optimizable interface +// but only activates on an edge case where the disjunction is a +// wrapper around a single Optimizable child searcher +func (s *DisjunctionSliceSearcher) Optimize(kind string, octx index.OptimizableContext) ( + index.OptimizableContext, error, +) { + if len(s.searchers) == 1 { + o, ok := s.searchers[0].(index.Optimizable) + if ok { + return o.Optimize(kind, octx) + } + } + + return nil, nil +} diff --git a/search/searcher/search_disjunction_test.go b/search/searcher/search_disjunction_test.go new file mode 100644 index 0000000..fe219c8 --- /dev/null +++ b/search/searcher/search_disjunction_test.go @@ -0,0 +1,223 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestDisjunctionSearch(t *testing.T) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainTrue := search.SearcherOptions{Explain: true} + + martyTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + martyOrDustinSearcher, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + + martyTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher2, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + martyOrDustinSearcher2, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher2, dustinTermSearcher2}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + + raviTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "ravi", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + nestedRaviOrMartyOrDustinSearcher, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{raviTermSearcher, martyOrDustinSearcher2}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + searcher search.Searcher + results []*search.DocumentMatch + }{ + { + searcher: martyOrDustinSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 0.6775110856165737, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.6775110856165737, + }, + }, + }, + // test a nested disjunction + { + searcher: nestedRaviOrMartyOrDustinSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 0.2765927424732821, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.2765927424732821, + }, + { + IndexInternalID: index.IndexInternalID("4"), + Score: 0.5531854849465642, + }, + }, + }, + } + + for testIndex, test := range tests { + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(test.searcher.DocumentMatchPoolSize(), 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + if i < len(test.results) { + if !next.IndexInternalID.Equals(test.results[i].IndexInternalID) { + t.Errorf("expected result %d to have id %s got %s for test %d", i, test.results[i].IndexInternalID, next.IndexInternalID, testIndex) + } + if !scoresCloseEnough(next.Score, test.results[i].Score) { + t.Errorf("expected result %d to have score %v got %v for test %d", i, test.results[i].Score, next.Score, testIndex) + t.Logf("scoring explanation: %s", next.Expl) + } + } + ctx.DocumentMatchPool.Put(next) + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.results) != i { + t.Errorf("expected %d results got %d for test %d", len(test.results), i, testIndex) + } + } +} + +func TestDisjunctionAdvance(t *testing.T) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainTrue := search.SearcherOptions{Explain: true} + + martyTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + martyOrDustinSearcher, err := NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher}, 0, explainTrue) + if err != nil { + t.Fatal(err) + } + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(martyOrDustinSearcher.DocumentMatchPoolSize(), 0), + } + match, err := martyOrDustinSearcher.Advance(ctx, index.IndexInternalID("3")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if match == nil { + t.Errorf("expected 3, got nil") + } +} + +func TestDisjunctionSearchTooMany(t *testing.T) { + // set to max to a low non-zero value + DisjunctionMaxClauseCount = 2 + defer func() { + // reset it after the test + DisjunctionMaxClauseCount = 0 + }() + + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainTrue := search.SearcherOptions{Explain: true} + + martyTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "marty", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + dustinTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "dustin", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + steveTermSearcher, err := NewTermSearcher(context.TODO(), twoDocIndexReader, "steve", "name", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + _, err = NewDisjunctionSearcher(context.TODO(), twoDocIndexReader, []search.Searcher{martyTermSearcher, dustinTermSearcher, steveTermSearcher}, 0, explainTrue) + if err == nil { + t.Fatal(err) + } +} diff --git a/search/searcher/search_docid.go b/search/searcher/search_docid.go new file mode 100644 index 0000000..720fd32 --- /dev/null +++ b/search/searcher/search_docid.go @@ -0,0 +1,110 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeDocIDSearcher int + +func init() { + var ds DocIDSearcher + reflectStaticSizeDocIDSearcher = int(reflect.TypeOf(ds).Size()) +} + +// DocIDSearcher returns documents matching a predefined set of identifiers. +type DocIDSearcher struct { + reader index.DocIDReader + scorer *scorer.ConstantScorer + count int +} + +func NewDocIDSearcher(ctx context.Context, indexReader index.IndexReader, ids []string, boost float64, + options search.SearcherOptions) (searcher *DocIDSearcher, err error) { + + reader, err := indexReader.DocIDReaderOnly(ids) + if err != nil { + return nil, err + } + scorer := scorer.NewConstantScorer(1.0, boost, options) + return &DocIDSearcher{ + scorer: scorer, + reader: reader, + count: len(ids), + }, nil +} + +func (s *DocIDSearcher) Size() int { + return reflectStaticSizeDocIDSearcher + size.SizeOfPtr + + s.reader.Size() + + s.scorer.Size() +} + +func (s *DocIDSearcher) Count() uint64 { + return uint64(s.count) +} + +func (s *DocIDSearcher) Weight() float64 { + return s.scorer.Weight() +} + +func (s *DocIDSearcher) SetQueryNorm(qnorm float64) { + s.scorer.SetQueryNorm(qnorm) +} + +func (s *DocIDSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + docidMatch, err := s.reader.Next() + if err != nil { + return nil, err + } + if docidMatch == nil { + return nil, nil + } + + docMatch := s.scorer.Score(ctx, docidMatch) + return docMatch, nil +} + +func (s *DocIDSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + docidMatch, err := s.reader.Advance(ID) + if err != nil { + return nil, err + } + if docidMatch == nil { + return nil, nil + } + + docMatch := s.scorer.Score(ctx, docidMatch) + return docMatch, nil +} + +func (s *DocIDSearcher) Close() error { + return s.reader.Close() +} + +func (s *DocIDSearcher) Min() int { + return 0 +} + +func (s *DocIDSearcher) DocumentMatchPoolSize() int { + return 1 +} diff --git a/search/searcher/search_docid_test.go b/search/searcher/search_docid_test.go new file mode 100644 index 0000000..65fedc6 --- /dev/null +++ b/search/searcher/search_docid_test.go @@ -0,0 +1,146 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func testDocIDSearcher(t *testing.T, indexed, searched, wanted []string) { + analysisQueue := index.NewAnalysisQueue(1) + i, err := upsidedown.NewUpsideDownCouch( + gtreap.Name, + map[string]interface{}{ + "path": "", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + for _, id := range indexed { + doc := document.NewDocument(id) + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + } + + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainOff := search.SearcherOptions{Explain: false} + + searcher, err := NewDocIDSearcher(context.TODO(), indexReader, searched, 1.0, explainOff) + if err != nil { + t.Fatal(err) + } + defer func() { + err := searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(searcher.DocumentMatchPoolSize(), 0), + } + + // Check the sequence + for i, id := range wanted { + m, err := searcher.Next(ctx) + if err != nil { + t.Fatal(err) + } + if !index.IndexInternalID(id).Equals(m.IndexInternalID) { + t.Fatalf("expected %v at position %v, got %v", id, i, m.IndexInternalID) + } + ctx.DocumentMatchPool.Put(m) + } + m, err := searcher.Next(ctx) + if err != nil { + t.Fatal(err) + } + if m != nil { + t.Fatalf("expected nil past the end of the sequence, got %v", m.IndexInternalID) + } + ctx.DocumentMatchPool.Put(m) + + // Check seeking + for _, id := range wanted { + if len(id) != 2 { + t.Fatalf("expected identifier must be 2 characters long, got %v", id) + } + before := id[:1] + for _, target := range []string{before, id} { + m, err := searcher.Advance(ctx, index.IndexInternalID(target)) + if err != nil { + t.Fatal(err) + } + if m == nil || !m.IndexInternalID.Equals(index.IndexInternalID(id)) { + t.Fatalf("advancing to %v returned %v instead of %v", before, m, id) + } + ctx.DocumentMatchPool.Put(m) + } + } + // Seek after the end of the sequence + after := "zzz" + m, err = searcher.Advance(ctx, index.IndexInternalID(after)) + if err != nil { + t.Fatal(err) + } + if m != nil { + t.Fatalf("advancing past the end of the sequence should return nil, got %v", m) + } + ctx.DocumentMatchPool.Put(m) +} + +func TestDocIDSearcherEmptySearchEmptyIndex(t *testing.T) { + testDocIDSearcher(t, nil, nil, nil) +} + +func TestDocIDSearcherEmptyIndex(t *testing.T) { + testDocIDSearcher(t, nil, []string{"aa", "bb"}, nil) +} + +func TestDocIDSearcherEmptySearch(t *testing.T) { + testDocIDSearcher(t, []string{"aa", "bb"}, nil, nil) +} + +func TestDocIDSearcherValid(t *testing.T) { + // Test missing, out of order and duplicate inputs + testDocIDSearcher(t, []string{"aa", "bb", "cc"}, + []string{"ee", "bb", "aa", "bb"}, + []string{"aa", "bb"}) +} diff --git a/search/searcher/search_filter.go b/search/searcher/search_filter.go new file mode 100644 index 0000000..4e4dd5e --- /dev/null +++ b/search/searcher/search_filter.go @@ -0,0 +1,104 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeFilteringSearcher int + +func init() { + var fs FilteringSearcher + reflectStaticSizeFilteringSearcher = int(reflect.TypeOf(fs).Size()) +} + +// FilterFunc defines a function which can filter documents +// returning true means keep the document +// returning false means do not keep the document +type FilterFunc func(d *search.DocumentMatch) bool + +// FilteringSearcher wraps any other searcher, but checks any Next/Advance +// call against the supplied FilterFunc +type FilteringSearcher struct { + child search.Searcher + accept FilterFunc +} + +func NewFilteringSearcher(ctx context.Context, s search.Searcher, filter FilterFunc) *FilteringSearcher { + return &FilteringSearcher{ + child: s, + accept: filter, + } +} + +func (f *FilteringSearcher) Size() int { + return reflectStaticSizeFilteringSearcher + size.SizeOfPtr + + f.child.Size() +} + +func (f *FilteringSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + next, err := f.child.Next(ctx) + for next != nil && err == nil { + if f.accept(next) { + return next, nil + } + next, err = f.child.Next(ctx) + } + return nil, err +} + +func (f *FilteringSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + adv, err := f.child.Advance(ctx, ID) + if err != nil { + return nil, err + } + if adv == nil { + return nil, nil + } + if f.accept(adv) { + return adv, nil + } + return f.Next(ctx) +} + +func (f *FilteringSearcher) Close() error { + return f.child.Close() +} + +func (f *FilteringSearcher) Weight() float64 { + return f.child.Weight() +} + +func (f *FilteringSearcher) SetQueryNorm(n float64) { + f.child.SetQueryNorm(n) +} + +func (f *FilteringSearcher) Count() uint64 { + return f.child.Count() +} + +func (f *FilteringSearcher) Min() int { + return f.child.Min() +} + +func (f *FilteringSearcher) DocumentMatchPoolSize() int { + return f.child.DocumentMatchPoolSize() +} diff --git a/search/searcher/search_fuzzy.go b/search/searcher/search_fuzzy.go new file mode 100644 index 0000000..187486e --- /dev/null +++ b/search/searcher/search_fuzzy.go @@ -0,0 +1,250 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "fmt" + "strings" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +var MaxFuzziness = 2 + +// AutoFuzzinessHighThreshold is the threshold for the term length +// above which the fuzziness is set to MaxFuzziness when the fuzziness +// mode is set to AutoFuzziness. +var AutoFuzzinessHighThreshold = 5 + +// AutoFuzzinessLowThreshold is the threshold for the term length +// below which the fuzziness is set to zero when the fuzziness mode +// is set to AutoFuzziness. +// For terms with length between AutoFuzzinessLowThreshold and +// AutoFuzzinessHighThreshold, the fuzziness is set to +// MaxFuzziness - 1. +var AutoFuzzinessLowThreshold = 2 + +func NewFuzzySearcher(ctx context.Context, indexReader index.IndexReader, term string, + prefix, fuzziness int, field string, boost float64, + options search.SearcherOptions) (search.Searcher, error) { + + if fuzziness > MaxFuzziness { + return nil, fmt.Errorf("fuzziness exceeds max (%d)", MaxFuzziness) + } + + if fuzziness < 0 { + return nil, fmt.Errorf("invalid fuzziness, negative") + } + if fuzziness == 0 { + // no fuzziness, just do a term search + // check if the call is made from a phrase searcher + // and if so, add the term to the fuzzy term matches + // since the fuzzy candidate terms are not collected + // for a term search, and the only candidate term is + // the term itself + if ctx != nil { + fuzzyTermMatches := ctx.Value(search.FuzzyMatchPhraseKey) + if fuzzyTermMatches != nil { + fuzzyTermMatches.(map[string][]string)[term] = []string{term} + } + } + return NewTermSearcher(ctx, indexReader, term, field, boost, options) + } + + // Note: we don't byte slice the term for a prefix because of runes. + prefixTerm := "" + for i, r := range term { + if i < prefix { + prefixTerm += string(r) + } else { + break + } + } + fuzzyCandidates, err := findFuzzyCandidateTerms(ctx, indexReader, term, fuzziness, + field, prefixTerm) + if err != nil { + return nil, err + } + + var candidates []string + var editDistances []uint8 + var dictBytesRead uint64 + if fuzzyCandidates != nil { + candidates = fuzzyCandidates.candidates + editDistances = fuzzyCandidates.editDistances + dictBytesRead = fuzzyCandidates.bytesRead + } + + if ctx != nil { + reportIOStats(ctx, dictBytesRead) + search.RecordSearchCost(ctx, search.AddM, dictBytesRead) + fuzzyTermMatches := ctx.Value(search.FuzzyMatchPhraseKey) + if fuzzyTermMatches != nil { + fuzzyTermMatches.(map[string][]string)[term] = candidates + } + } + // check if the candidates are empty or have one term which is the term itself + if len(candidates) == 0 || (len(candidates) == 1 && candidates[0] == term) { + if ctx != nil { + fuzzyTermMatches := ctx.Value(search.FuzzyMatchPhraseKey) + if fuzzyTermMatches != nil { + fuzzyTermMatches.(map[string][]string)[term] = []string{term} + } + } + return NewTermSearcher(ctx, indexReader, term, field, boost, options) + } + + return NewMultiTermSearcherBoosted(ctx, indexReader, candidates, field, + boost, editDistances, options, true) +} + +func GetAutoFuzziness(term string) int { + termLength := len(term) + if termLength > AutoFuzzinessHighThreshold { + return MaxFuzziness + } else if termLength > AutoFuzzinessLowThreshold { + return MaxFuzziness - 1 + } + return 0 +} + +func NewAutoFuzzySearcher(ctx context.Context, indexReader index.IndexReader, term string, + prefix int, field string, boost float64, options search.SearcherOptions) (search.Searcher, error) { + return NewFuzzySearcher(ctx, indexReader, term, prefix, GetAutoFuzziness(term), field, boost, options) +} + +type fuzzyCandidates struct { + candidates []string + editDistances []uint8 + bytesRead uint64 +} + +func reportIOStats(ctx context.Context, bytesRead uint64) { + // The fuzzy, regexp like queries essentially load a dictionary, + // which potentially incurs a cost that must be accounted by + // using the callback to report the value. + if ctx != nil { + statsCallbackFn := ctx.Value(search.SearchIOStatsCallbackKey) + if statsCallbackFn != nil { + statsCallbackFn.(search.SearchIOStatsCallbackFunc)(bytesRead) + } + } +} + +func findFuzzyCandidateTerms(ctx context.Context, indexReader index.IndexReader, term string, + fuzziness int, field, prefixTerm string) (rv *fuzzyCandidates, err error) { + rv = &fuzzyCandidates{ + candidates: make([]string, 0), + editDistances: make([]uint8, 0), + } + + // in case of advanced reader implementations directly call + // the levenshtein automaton based iterator to collect the + // candidate terms + if ir, ok := indexReader.(index.IndexReaderFuzzy); ok { + termSet := make(map[string]struct{}) + addCandidateTerm := func(term string, editDistance uint8) error { + if _, exists := termSet[term]; !exists { + termSet[term] = struct{}{} + rv.candidates = append(rv.candidates, term) + rv.editDistances = append(rv.editDistances, editDistance) + if tooManyClauses(len(rv.candidates)) { + return tooManyClausesErr(field, len(rv.candidates)) + } + } + return nil + } + fieldDict, a, err := ir.FieldDictFuzzyAutomaton(field, term, fuzziness, prefixTerm) + if err != nil { + return nil, err + } + defer func() { + if cerr := fieldDict.Close(); cerr != nil && err == nil { + err = cerr + } + }() + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + err = addCandidateTerm(tfd.Term, tfd.EditDistance) + if err != nil { + return nil, err + } + tfd, err = fieldDict.Next() + } + if err != nil { + return nil, err + } + if ctx != nil { + if fts, ok := ctx.Value(search.FieldTermSynonymMapKey).(search.FieldTermSynonymMap); ok { + if ts, exists := fts[field]; exists { + for term := range ts { + if _, exists := termSet[term]; exists { + continue + } + if !strings.HasPrefix(term, prefixTerm) { + continue + } + match, editDistance := a.MatchAndDistance(term) + if match { + err = addCandidateTerm(term, editDistance) + if err != nil { + return nil, err + } + } + } + } + } + } + rv.bytesRead = fieldDict.BytesRead() + return rv, nil + } + + var fieldDict index.FieldDict + if len(prefixTerm) > 0 { + fieldDict, err = indexReader.FieldDictPrefix(field, []byte(prefixTerm)) + } else { + fieldDict, err = indexReader.FieldDict(field) + } + if err != nil { + return nil, err + } + defer func() { + if cerr := fieldDict.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + // enumerate terms and check levenshtein distance + var reuse []int + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + var ld int + var exceeded bool + ld, exceeded, reuse = search.LevenshteinDistanceMaxReuseSlice(term, tfd.Term, fuzziness, reuse) + if !exceeded && ld <= fuzziness { + rv.candidates = append(rv.candidates, tfd.Term) + rv.editDistances = append(rv.editDistances, uint8(ld)) + if tooManyClauses(len(rv.candidates)) { + return nil, tooManyClausesErr(field, len(rv.candidates)) + } + } + tfd, err = fieldDict.Next() + } + + rv.bytesRead = fieldDict.BytesRead() + return rv, err +} diff --git a/search/searcher/search_fuzzy_test.go b/search/searcher/search_fuzzy_test.go new file mode 100644 index 0000000..f6c2b79 --- /dev/null +++ b/search/searcher/search_fuzzy_test.go @@ -0,0 +1,156 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestFuzzySearch(t *testing.T) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainTrue := search.SearcherOptions{Explain: true} + + fuzzySearcherbeet, err := NewFuzzySearcher(context.TODO(), twoDocIndexReader, "beet", 0, 1, "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + + fuzzySearcherdouches, err := NewFuzzySearcher(context.TODO(), twoDocIndexReader, "douches", 0, 2, "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + + fuzzySearcheraplee, err := NewFuzzySearcher(context.TODO(), twoDocIndexReader, "aplee", 0, 2, "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + + fuzzySearcherprefix, err := NewFuzzySearcher(context.TODO(), twoDocIndexReader, "water", 3, 2, "desc", 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + searcher search.Searcher + results []*search.DocumentMatch + }{ + { + searcher: fuzzySearcherbeet, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("2"), + Score: 0.5, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.5, + }, + { + IndexInternalID: index.IndexInternalID("4"), + Score: 0.9999999838027345, + }, + }, + }, + { + searcher: fuzzySearcherdouches, + results: []*search.DocumentMatch{}, + }, + { + searcher: fuzzySearcheraplee, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("3"), + Score: 0.9581453659370776, + }, + }, + }, + { + searcher: fuzzySearcherprefix, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("5"), + Score: 1.916290731874155, + }, + }, + }, + } + + for testIndex, test := range tests { + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(test.searcher.DocumentMatchPoolSize(), 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + if i < len(test.results) { + if !next.IndexInternalID.Equals(test.results[i].IndexInternalID) { + t.Errorf("expected result %d to have id %s got %s for test %d", i, test.results[i].IndexInternalID, next.IndexInternalID, testIndex) + } + if next.Score != test.results[i].Score { + t.Errorf("expected result %d to have score %v got %v for test %d", i, test.results[i].Score, next.Score, testIndex) + t.Logf("scoring explanation: %s", next.Expl) + } + } + ctx.DocumentMatchPool.Put(next) + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.results) != i { + t.Errorf("expected %d results got %d for test %d", len(test.results), i, testIndex) + } + } +} + +func TestFuzzySearchLimitErrors(t *testing.T) { + explainTrue := search.SearcherOptions{Explain: true} + _, err := NewFuzzySearcher(context.TODO(), nil, "water", 3, 3, "desc", 1.0, explainTrue) + if err == nil { + t.Fatal("`fuzziness exceeds max (2)` error expected") + } + + _, err = NewFuzzySearcher(context.TODO(), nil, "water", 3, -1, "desc", 1.0, explainTrue) + if err == nil { + t.Fatal("`invalid fuzziness, negative` error expected") + } +} diff --git a/search/searcher/search_geoboundingbox.go b/search/searcher/search_geoboundingbox.go new file mode 100644 index 0000000..f9dcf16 --- /dev/null +++ b/search/searcher/search_geoboundingbox.go @@ -0,0 +1,306 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +type filterFunc func(key []byte) bool + +var ( + GeoBitsShift1 = geo.GeoBits << 1 + GeoBitsShift1Minus1 = GeoBitsShift1 - 1 +) + +func NewGeoBoundingBoxSearcher(ctx context.Context, indexReader index.IndexReader, minLon, minLat, + maxLon, maxLat float64, field string, boost float64, + options search.SearcherOptions, checkBoundaries bool) ( + search.Searcher, error, +) { + if tp, ok := indexReader.(index.SpatialIndexPlugin); ok { + sp, err := tp.GetSpatialAnalyzerPlugin("s2") + if err == nil { + terms := sp.GetQueryTokens(geo.NewBoundedRectangle(minLat, + minLon, maxLat, maxLon)) + boxSearcher, err := NewMultiTermSearcher(ctx, indexReader, + terms, field, boost, options, false) + if err != nil { + return nil, err + } + + dvReader, err := indexReader.DocValueReader([]string{field}) + if err != nil { + return nil, err + } + + return NewFilteringSearcher(ctx, boxSearcher, buildRectFilter(ctx, dvReader, + field, minLon, minLat, maxLon, maxLat)), nil + } + } + + // indexes without the spatial plugin override would continue here. + + // track list of opened searchers, for cleanup on early exit + var openedSearchers []search.Searcher + cleanupOpenedSearchers := func() { + for _, s := range openedSearchers { + _ = s.Close() + } + } + + // do math to produce list of terms needed for this search + onBoundaryTerms, notOnBoundaryTerms, err := ComputeGeoRange(context.TODO(), 0, GeoBitsShift1Minus1, + minLon, minLat, maxLon, maxLat, checkBoundaries, indexReader, field) + if err != nil { + return nil, err + } + + var onBoundarySearcher search.Searcher + dvReader, err := indexReader.DocValueReader([]string{field}) + if err != nil { + return nil, err + } + + if len(onBoundaryTerms) > 0 { + rawOnBoundarySearcher, err := NewMultiTermSearcherBytes(ctx, indexReader, + onBoundaryTerms, field, boost, options, false) + if err != nil { + return nil, err + } + // add filter to check points near the boundary + onBoundarySearcher = NewFilteringSearcher(ctx, rawOnBoundarySearcher, + buildRectFilter(ctx, dvReader, field, minLon, minLat, maxLon, maxLat)) + openedSearchers = append(openedSearchers, onBoundarySearcher) + } + + var notOnBoundarySearcher search.Searcher + if len(notOnBoundaryTerms) > 0 { + var err error + notOnBoundarySearcher, err = NewMultiTermSearcherBytes(ctx, indexReader, + notOnBoundaryTerms, field, boost, options, false) + if err != nil { + cleanupOpenedSearchers() + return nil, err + } + openedSearchers = append(openedSearchers, notOnBoundarySearcher) + } + + if onBoundarySearcher != nil && notOnBoundarySearcher != nil { + rv, err := NewDisjunctionSearcher(ctx, indexReader, + []search.Searcher{ + onBoundarySearcher, + notOnBoundarySearcher, + }, + 0, options) + if err != nil { + cleanupOpenedSearchers() + return nil, err + } + return rv, nil + } else if onBoundarySearcher != nil { + return onBoundarySearcher, nil + } else if notOnBoundarySearcher != nil { + return notOnBoundarySearcher, nil + } + + return NewMatchNoneSearcher(indexReader) +} + +var ( + geoMaxShift = document.GeoPrecisionStep * 4 + geoDetailLevel = ((geo.GeoBits << 1) - geoMaxShift) / 2 +) + +type closeFunc func() error + +func ComputeGeoRange(ctx context.Context, term uint64, shift uint, + sminLon, sminLat, smaxLon, smaxLat float64, checkBoundaries bool, + indexReader index.IndexReader, field string) ( + onBoundary [][]byte, notOnBoundary [][]byte, err error, +) { + isIndexed, closeF, err := buildIsIndexedFunc(ctx, indexReader, field) + if closeF != nil { + defer func() { + cerr := closeF() + if cerr != nil { + err = cerr + } + }() + } + + grc := &geoRangeCompute{ + preallocBytesLen: 32, + preallocBytes: make([]byte, 32), + sminLon: sminLon, + sminLat: sminLat, + smaxLon: smaxLon, + smaxLat: smaxLat, + checkBoundaries: checkBoundaries, + isIndexed: isIndexed, + } + + grc.computeGeoRange(term, shift) + + return grc.onBoundary, grc.notOnBoundary, nil +} + +func buildIsIndexedFunc(ctx context.Context, indexReader index.IndexReader, field string) (isIndexed filterFunc, closeF closeFunc, err error) { + if irr, ok := indexReader.(index.IndexReaderContains); ok { + fieldDict, err := irr.FieldDictContains(field) + if err != nil { + return nil, nil, err + } + + isIndexed = func(term []byte) bool { + found, err := fieldDict.Contains(term) + return err == nil && found + } + + closeF = func() error { + if fd, ok := fieldDict.(index.FieldDict); ok { + err := fd.Close() + if err != nil { + return err + } + } + return nil + } + } else if indexReader != nil { + isIndexed = func(term []byte) bool { + reader, err := indexReader.TermFieldReader(ctx, term, field, false, false, false) + if err != nil || reader == nil { + return false + } + if reader.Count() == 0 { + _ = reader.Close() + return false + } + _ = reader.Close() + return true + } + } else { + isIndexed = func([]byte) bool { + return true + } + } + return isIndexed, closeF, err +} + +func buildRectFilter(ctx context.Context, dvReader index.DocValueReader, field string, + minLon, minLat, maxLon, maxLat float64, +) FilterFunc { + return func(d *search.DocumentMatch) bool { + // check geo matches against all numeric type terms indexed + var lons, lats []float64 + var found bool + err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) { + // only consider the values which are shifted 0 + prefixCoded := numeric.PrefixCoded(term) + shift, err := prefixCoded.Shift() + if err == nil && shift == 0 { + var i64 int64 + i64, err = prefixCoded.Int64() + if err == nil { + lons = append(lons, geo.MortonUnhashLon(uint64(i64))) + lats = append(lats, geo.MortonUnhashLat(uint64(i64))) + found = true + } + } + }) + if err == nil && found { + bytes := dvReader.BytesRead() + if bytes > 0 { + reportIOStats(ctx, bytes) + search.RecordSearchCost(ctx, search.AddM, bytes) + } + for i := range lons { + if geo.BoundingBoxContains(lons[i], lats[i], + minLon, minLat, maxLon, maxLat) { + return true + } + } + } + return false + } +} + +type geoRangeCompute struct { + preallocBytesLen int + preallocBytes []byte + sminLon, sminLat, smaxLon, smaxLat float64 + checkBoundaries bool + onBoundary, notOnBoundary [][]byte + isIndexed func(term []byte) bool +} + +func (grc *geoRangeCompute) makePrefixCoded(in int64, shift uint) (rv numeric.PrefixCoded) { + if len(grc.preallocBytes) <= 0 { + grc.preallocBytesLen = grc.preallocBytesLen * 2 + grc.preallocBytes = make([]byte, grc.preallocBytesLen) + } + + rv, grc.preallocBytes, _ = numeric.NewPrefixCodedInt64Prealloc(in, shift, grc.preallocBytes) + + return rv +} + +func (grc *geoRangeCompute) computeGeoRange(term uint64, shift uint) { + split := term | uint64(0x1)<> 1 + + within := res%document.GeoPrecisionStep == 0 && + geo.RectWithin(minLon, minLat, maxLon, maxLat, + grc.sminLon, grc.sminLat, grc.smaxLon, grc.smaxLat) + if within || (level == geoDetailLevel && + geo.RectIntersects(minLon, minLat, maxLon, maxLat, + grc.sminLon, grc.sminLat, grc.smaxLon, grc.smaxLat)) { + codedTerm := grc.makePrefixCoded(int64(start), res) + if grc.isIndexed(codedTerm) { + if !within && grc.checkBoundaries { + grc.onBoundary = append(grc.onBoundary, codedTerm) + } else { + grc.notOnBoundary = append(grc.notOnBoundary, codedTerm) + } + } + } else if level < geoDetailLevel && + geo.RectIntersects(minLon, minLat, maxLon, maxLat, + grc.sminLon, grc.sminLat, grc.smaxLon, grc.smaxLat) { + grc.computeGeoRange(start, res-1) + } +} diff --git a/search/searcher/search_geoboundingbox_test.go b/search/searcher/search_geoboundingbox_test.go new file mode 100644 index 0000000..4715b5b --- /dev/null +++ b/search/searcher/search_geoboundingbox_test.go @@ -0,0 +1,317 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoBoundingBox(t *testing.T) { + tests := []struct { + minLon float64 + minLat float64 + maxLon float64 + maxLat float64 + field string + want []string + }{ + {10.001, 10.001, 20.002, 20.002, "loc", nil}, + {0.001, 0.001, 0.002, 0.002, "loc", []string{"a"}}, + {0.001, 0.001, 1.002, 1.002, "loc", []string{"a", "b"}}, + {0.001, 0.001, 9.002, 9.002, "loc", []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}}, + // same upper-left, bottom-right point + {25, 25, 25, 25, "loc", nil}, + // box that would return points, but points reversed + {0.002, 0.002, 0.001, 0.001, "loc", nil}, + } + + i := setupGeo(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for _, test := range tests { + got, err := testGeoBoundingBoxSearch(indexReader, test.minLon, test.minLat, test.maxLon, test.maxLat, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("expected %v, got %v for %f %f %f %f %s", test.want, got, test.minLon, test.minLat, test.maxLon, test.maxLat, test.field) + } + + } +} + +func testGeoBoundingBoxSearch(i index.IndexReader, minLon, minLat, maxLon, maxLat float64, field string) ([]string, error) { + var rv []string + gbs, err := NewGeoBoundingBoxSearcher(context.TODO(), i, minLon, minLat, maxLon, maxLat, field, 1.0, search.SearcherOptions{}, true) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + rv = append(rv, string(docMatch.IndexInternalID)) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeo(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := upsidedown.NewUpsideDownCouch( + gtreap.Name, + map[string]interface{}{ + "path": "", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + doc := document.NewDocument("a") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 0.0015, 0.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("b") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 1.0015, 1.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("c") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 2.0015, 2.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("d") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 3.0015, 3.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("e") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 4.0015, 4.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("f") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 5.0015, 5.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("g") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 6.0015, 6.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("h") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 7.0015, 7.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("i") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 8.0015, 8.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("j") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 9.0015, 9.0015)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} + +func TestComputeGeoRange(t *testing.T) { + tests := []struct { + degs float64 + onBoundary int + offBoundary int + err string + }{ + {0.01, 4, 0, ""}, + {0.1, 56, 144, ""}, + {100.0, 32768, 258560, ""}, + } + + for testi, test := range tests { + onBoundaryRes, offBoundaryRes, err := ComputeGeoRange(context.TODO(), 0, GeoBitsShift1Minus1, + -1.0*test.degs, -1.0*test.degs, test.degs, test.degs, true, nil, "") + if (err != nil) != (test.err != "") { + t.Errorf("test: %+v, err: %v", test, err) + } + if len(onBoundaryRes) != test.onBoundary { + t.Errorf("test: %+v, onBoundaryRes: %v", test, len(onBoundaryRes)) + } + if len(offBoundaryRes) != test.offBoundary { + t.Errorf("test: %+v, offBoundaryRes: %v", test, len(offBoundaryRes)) + } + + onBROrig, offBROrig := origComputeGeoRange(0, GeoBitsShift1Minus1, + -1.0*test.degs, -1.0*test.degs, test.degs, test.degs, true) + if !reflect.DeepEqual(onBoundaryRes, onBROrig) { + t.Errorf("testi: %d, test: %+v, onBoundaryRes != onBROrig,\n onBoundaryRes:%v,\n onBROrig: %v", + testi, test, onBoundaryRes, onBROrig) + } + if !reflect.DeepEqual(offBoundaryRes, offBROrig) { + t.Errorf("testi: %d, test: %+v, offBoundaryRes, offBROrig,\n offBoundaryRes: %v,\n offBROrig: %v", + testi, test, offBoundaryRes, offBROrig) + } + } +} + +// -------------------------------------------------------------------- + +func BenchmarkComputeGeoRangePt01(b *testing.B) { + onBoundary := 4 + offBoundary := 0 + benchmarkComputeGeoRange(b, -0.01, -0.01, 0.01, 0.01, onBoundary, offBoundary) +} + +func BenchmarkComputeGeoRangePt1(b *testing.B) { + onBoundary := 56 + offBoundary := 144 + benchmarkComputeGeoRange(b, -0.1, -0.1, 0.1, 0.1, onBoundary, offBoundary) +} + +func BenchmarkComputeGeoRange10(b *testing.B) { + onBoundary := 5464 + offBoundary := 53704 + benchmarkComputeGeoRange(b, -10.0, -10.0, 10.0, 10.0, onBoundary, offBoundary) +} + +func BenchmarkComputeGeoRange100(b *testing.B) { + onBoundary := 32768 + offBoundary := 258560 + benchmarkComputeGeoRange(b, -100.0, -100.0, 100.0, 100.0, onBoundary, offBoundary) +} + +// -------------------------------------------------------------------- + +func benchmarkComputeGeoRange(b *testing.B, + minLon, minLat, maxLon, maxLat float64, onBoundary, offBoundary int, +) { + checkBoundaries := true + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + onBoundaryRes, offBoundaryRes, err := ComputeGeoRange(context.TODO(), 0, GeoBitsShift1Minus1, minLon, minLat, maxLon, maxLat, checkBoundaries, nil, "") + if err != nil { + b.Fatalf("expected no err") + } + if len(onBoundaryRes) != onBoundary || len(offBoundaryRes) != offBoundary { + b.Fatalf("boundaries not matching") + } + } +} + +// -------------------------------------------------------------------- + +// original, non-optimized implementation of ComputeGeoRange +func origComputeGeoRange(term uint64, shift uint, + sminLon, sminLat, smaxLon, smaxLat float64, + checkBoundaries bool) ( + onBoundary [][]byte, notOnBoundary [][]byte, +) { + split := term | uint64(0x1)<> 1 + + within := res%document.GeoPrecisionStep == 0 && + geo.RectWithin(minLon, minLat, maxLon, maxLat, + sminLon, sminLat, smaxLon, smaxLat) + if within || (level == geoDetailLevel && + geo.RectIntersects(minLon, minLat, maxLon, maxLat, + sminLon, sminLat, smaxLon, smaxLat)) { + if !within && checkBoundaries { + return [][]byte{ + numeric.MustNewPrefixCodedInt64(int64(start), res), + }, nil + } + return nil, + [][]byte{ + numeric.MustNewPrefixCodedInt64(int64(start), res), + } + } else if level < geoDetailLevel && + geo.RectIntersects(minLon, minLat, maxLon, maxLat, + sminLon, sminLat, smaxLon, smaxLat) { + return origComputeGeoRange(start, res-1, sminLon, sminLat, smaxLon, smaxLat, + checkBoundaries) + } + return nil, nil +} diff --git a/search/searcher/search_geopointdistance.go b/search/searcher/search_geopointdistance.go new file mode 100644 index 0000000..fbe9589 --- /dev/null +++ b/search/searcher/search_geopointdistance.go @@ -0,0 +1,151 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func NewGeoPointDistanceSearcher(ctx context.Context, indexReader index.IndexReader, centerLon, + centerLat, dist float64, field string, boost float64, + options search.SearcherOptions) (search.Searcher, error) { + var rectSearcher search.Searcher + if tp, ok := indexReader.(index.SpatialIndexPlugin); ok { + sp, err := tp.GetSpatialAnalyzerPlugin("s2") + if err == nil { + terms := sp.GetQueryTokens(geo.NewPointDistance(centerLat, + centerLon, dist)) + rectSearcher, err = NewMultiTermSearcher(ctx, indexReader, terms, + field, boost, options, false) + if err != nil { + return nil, err + } + } + } + + // indexes without the spatial plugin override would get + // initialized here. + if rectSearcher == nil { + // compute bounding box containing the circle + topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, err := + geo.RectFromPointDistance(centerLon, centerLat, dist) + if err != nil { + return nil, err + } + + // build a searcher for the box + rectSearcher, err = boxSearcher(ctx, indexReader, + topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, + field, boost, options, false) + if err != nil { + return nil, err + } + } + + dvReader, err := indexReader.DocValueReader([]string{field}) + if err != nil { + return nil, err + } + + // wrap it in a filtering searcher which checks the actual distance + return NewFilteringSearcher(ctx, rectSearcher, + buildDistFilter(ctx, dvReader, field, centerLon, centerLat, dist)), nil +} + +// boxSearcher builds a searcher for the described bounding box +// if the desired box crosses the dateline, it is automatically split into +// two boxes joined through a disjunction searcher +func boxSearcher(ctx context.Context, indexReader index.IndexReader, + topLeftLon, topLeftLat, bottomRightLon, bottomRightLat float64, + field string, boost float64, options search.SearcherOptions, checkBoundaries bool) ( + search.Searcher, error) { + if bottomRightLon < topLeftLon { + // cross date line, rewrite as two parts + + leftSearcher, err := NewGeoBoundingBoxSearcher(ctx, indexReader, + -180, bottomRightLat, bottomRightLon, topLeftLat, + field, boost, options, checkBoundaries) + if err != nil { + return nil, err + } + rightSearcher, err := NewGeoBoundingBoxSearcher(ctx, indexReader, + topLeftLon, bottomRightLat, 180, topLeftLat, field, boost, options, + checkBoundaries) + if err != nil { + _ = leftSearcher.Close() + return nil, err + } + + boxSearcher, err := NewDisjunctionSearcher(ctx, indexReader, + []search.Searcher{leftSearcher, rightSearcher}, 0, options) + if err != nil { + _ = leftSearcher.Close() + _ = rightSearcher.Close() + return nil, err + } + return boxSearcher, nil + } + + // build geoboundingbox searcher for that bounding box + boxSearcher, err := NewGeoBoundingBoxSearcher(ctx, indexReader, + topLeftLon, bottomRightLat, bottomRightLon, topLeftLat, field, boost, + options, checkBoundaries) + if err != nil { + return nil, err + } + return boxSearcher, nil +} + +func buildDistFilter(ctx context.Context, dvReader index.DocValueReader, field string, + centerLon, centerLat, maxDist float64) FilterFunc { + return func(d *search.DocumentMatch) bool { + // check geo matches against all numeric type terms indexed + var lons, lats []float64 + var found bool + + err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) { + // only consider the values which are shifted 0 + prefixCoded := numeric.PrefixCoded(term) + shift, err := prefixCoded.Shift() + if err == nil && shift == 0 { + i64, err := prefixCoded.Int64() + if err == nil { + lons = append(lons, geo.MortonUnhashLon(uint64(i64))) + lats = append(lats, geo.MortonUnhashLat(uint64(i64))) + found = true + } + } + }) + if err == nil && found { + bytes := dvReader.BytesRead() + if bytes > 0 { + reportIOStats(ctx, bytes) + search.RecordSearchCost(ctx, search.AddM, bytes) + } + for i := range lons { + dist := geo.Haversin(lons[i], lats[i], centerLon, centerLat) + if dist <= maxDist/1000 { + return true + } + } + } + return false + } +} diff --git a/search/searcher/search_geopointdistance_test.go b/search/searcher/search_geopointdistance_test.go new file mode 100644 index 0000000..20f65c8 --- /dev/null +++ b/search/searcher/search_geopointdistance_test.go @@ -0,0 +1,157 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoPointDistanceSearcher(t *testing.T) { + tests := []struct { + centerLon float64 + centerLat float64 + dist float64 + field string + want []string + }{ + // approx 110567m per degree at equator + {0.0, 0.0, 0, "loc", nil}, + {0.0, 0.0, 110567, "loc", []string{"a"}}, + {0.0, 0.0, 2 * 110567, "loc", []string{"a", "b"}}, + // stretching our approximation here + {0.0, 0.0, 15 * 110567, "loc", []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}}, + } + + i := setupGeo(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for _, test := range tests { + got, err := testGeoPointDistanceSearch(indexReader, test.centerLon, test.centerLat, test.dist, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("expected %v, got %v for %f %f %f %s", test.want, got, test.centerLon, test.centerLat, test.dist, test.field) + } + + } +} + +func testGeoPointDistanceSearch(i index.IndexReader, centerLon, centerLat, dist float64, field string) ([]string, error) { + var rv []string + gds, err := NewGeoPointDistanceSearcher(context.TODO(), i, centerLon, centerLat, dist, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gds.DocumentMatchPoolSize(), 0), + } + docMatch, err := gds.Next(ctx) + for docMatch != nil && err == nil { + rv = append(rv, string(docMatch.IndexInternalID)) + docMatch, err = gds.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func TestGeoPointDistanceCompare(t *testing.T) { + tests := []struct { + docLat, docLon float64 + centerLat, centerLon float64 + distance string + }{ + // Data points originally from MB-33454. + { + docLat: 33.718, + docLon: -116.8293, + centerLat: 39.59000587, + centerLon: -119.22998428, + distance: "10000mi", + }, + { + docLat: 41.1305, + docLon: -121.6587, + centerLat: 61.28, + centerLon: -149.34, + distance: "10000mi", + }, + } + + for testi, test := range tests { + // compares the results from ComputeGeoRange with original, non-optimized version + compare := func(desc string, + minLon, minLat, maxLon, maxLat float64, checkBoundaries bool, + ) { + // do math to produce list of terms needed for this search + onBoundaryRes, offBoundaryRes, err := ComputeGeoRange(context.TODO(), 0, GeoBitsShift1Minus1, + minLon, minLat, maxLon, maxLat, checkBoundaries, nil, "") + if err != nil { + t.Fatal(err) + } + + onBROrig, offBROrig := origComputeGeoRange(0, GeoBitsShift1Minus1, + minLon, minLat, maxLon, maxLat, checkBoundaries) + if !reflect.DeepEqual(onBoundaryRes, onBROrig) { + t.Fatalf("testi: %d, test: %+v, desc: %s, onBoundaryRes != onBROrig,\n onBoundaryRes:%v,\n onBROrig: %v", + testi, test, desc, onBoundaryRes, onBROrig) + } + if !reflect.DeepEqual(offBoundaryRes, offBROrig) { + t.Fatalf("testi: %d, test: %+v, desc: %s, offBoundaryRes, offBROrig,\n offBoundaryRes: %v,\n offBROrig: %v", + testi, test, desc, offBoundaryRes, offBROrig) + } + } + + // follow the general approach of the GeoPointDistanceSearcher... + dist, err := geo.ParseDistance(test.distance) + if err != nil { + t.Fatal(err) + } + + topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, err := geo.RectFromPointDistance(test.centerLon, test.centerLat, dist) + if err != nil { + t.Fatal(err) + } + + if bottomRightLon < topLeftLon { + // crosses date line, rewrite as two parts + compare("-180/f", -180, bottomRightLat, bottomRightLon, topLeftLat, false) + compare("-180/t", -180, bottomRightLat, bottomRightLon, topLeftLat, true) + + compare("180/f", topLeftLon, bottomRightLat, 180, topLeftLat, false) + compare("180/t", topLeftLon, bottomRightLat, 180, topLeftLat, true) + } else { + compare("reg/f", topLeftLon, bottomRightLat, bottomRightLon, topLeftLat, false) + compare("reg/t", topLeftLon, bottomRightLat, bottomRightLon, topLeftLat, true) + } + } +} diff --git a/search/searcher/search_geopolygon.go b/search/searcher/search_geopolygon.go new file mode 100644 index 0000000..a43edaf --- /dev/null +++ b/search/searcher/search_geopolygon.go @@ -0,0 +1,149 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "fmt" + "math" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func NewGeoBoundedPolygonSearcher(ctx context.Context, indexReader index.IndexReader, + coordinates []geo.Point, field string, boost float64, + options search.SearcherOptions) (search.Searcher, error) { + if len(coordinates) < 3 { + return nil, fmt.Errorf("Too few points specified for the polygon boundary") + } + + var rectSearcher search.Searcher + if sr, ok := indexReader.(index.SpatialIndexPlugin); ok { + tp, err := sr.GetSpatialAnalyzerPlugin("s2") + if err == nil { + terms := tp.GetQueryTokens(geo.NewBoundedPolygon(coordinates)) + rectSearcher, err = NewMultiTermSearcher(ctx, indexReader, terms, + field, boost, options, false) + if err != nil { + return nil, err + } + } + } + + // indexes without the spatial plugin override would get + // initialized here. + if rectSearcher == nil { + // compute the bounding box enclosing the polygon + topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, err := + geo.BoundingRectangleForPolygon(coordinates) + if err != nil { + return nil, err + } + + // build a searcher for the bounding box on the polygon + rectSearcher, err = boxSearcher(ctx, indexReader, + topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, + field, boost, options, true) + if err != nil { + return nil, err + } + } + + dvReader, err := indexReader.DocValueReader([]string{field}) + if err != nil { + return nil, err + } + + // wrap it in a filtering searcher that checks for the polygon inclusivity + return NewFilteringSearcher(ctx, rectSearcher, + buildPolygonFilter(ctx, dvReader, field, coordinates)), nil +} + +const float64EqualityThreshold = 1e-6 + +func almostEqual(a, b float64) bool { + return math.Abs(a-b) <= float64EqualityThreshold +} + +// buildPolygonFilter returns true if the point lies inside the +// polygon. It is based on the ray-casting technique as referred +// here: https://wrf.ecse.rpi.edu/nikola/pubdetails/pnpoly.html +func buildPolygonFilter(ctx context.Context, dvReader index.DocValueReader, field string, + coordinates []geo.Point) FilterFunc { + return func(d *search.DocumentMatch) bool { + // check geo matches against all numeric type terms indexed + var lons, lats []float64 + var found bool + + err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) { + // only consider the values which are shifted 0 + prefixCoded := numeric.PrefixCoded(term) + shift, err := prefixCoded.Shift() + if err == nil && shift == 0 { + i64, err := prefixCoded.Int64() + if err == nil { + lons = append(lons, geo.MortonUnhashLon(uint64(i64))) + lats = append(lats, geo.MortonUnhashLat(uint64(i64))) + found = true + } + } + }) + + // Note: this approach works for points which are strictly inside + // the polygon. ie it might fail for certain points on the polygon boundaries. + if err == nil && found { + bytes := dvReader.BytesRead() + if bytes > 0 { + reportIOStats(ctx, bytes) + search.RecordSearchCost(ctx, search.AddM, bytes) + } + nVertices := len(coordinates) + if len(coordinates) < 3 { + return false + } + rayIntersectsSegment := func(point, a, b geo.Point) bool { + return (a.Lat > point.Lat) != (b.Lat > point.Lat) && + point.Lon < (b.Lon-a.Lon)*(point.Lat-a.Lat)/(b.Lat-a.Lat)+a.Lon + } + + for i := range lons { + pt := geo.Point{Lon: lons[i], Lat: lats[i]} + inside := rayIntersectsSegment(pt, coordinates[len(coordinates)-1], coordinates[0]) + // check for a direct vertex match + if almostEqual(coordinates[0].Lat, lats[i]) && + almostEqual(coordinates[0].Lon, lons[i]) { + return true + } + + for j := 1; j < nVertices; j++ { + if almostEqual(coordinates[j].Lat, lats[i]) && + almostEqual(coordinates[j].Lon, lons[i]) { + return true + } + if rayIntersectsSegment(pt, coordinates[j-1], coordinates[j]) { + inside = !inside + } + } + if inside { + return true + } + } + } + return false + } +} diff --git a/search/searcher/search_geopolygon_test.go b/search/searcher/search_geopolygon_test.go new file mode 100644 index 0000000..d7dd0e3 --- /dev/null +++ b/search/searcher/search_geopolygon_test.go @@ -0,0 +1,409 @@ +// Copyright (c) 2019 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestSimpleGeoPolygons(t *testing.T) { + tests := []struct { + polygon []geo.Point + field string + want []string + }{ + // test points inside a triangle & on vertices + // r, s - inside and t,u - on vertices. + {[]geo.Point{{Lon: 1.0, Lat: 1.0}, {Lon: 2.0, Lat: 1.9}, {Lon: 2.0, Lat: 1.0}}, "loc", []string{"r", "s", "t", "u"}}, + // non overlapping polygon for the indexed documents + {[]geo.Point{{Lon: 3.0, Lat: 1.0}, {Lon: 4.0, Lat: 2.5}, {Lon: 3.0, Lat: 2}}, "loc", nil}, + } + i := setupGeoPolygonPoints(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for _, test := range tests { + got, err := testGeoPolygonSearch(indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("expected %v, got %v for polygon: %+v", test.want, got, test.polygon) + } + } +} + +func TestRealGeoPolygons(t *testing.T) { + tests := []struct { + polygon []geo.Point + field string + want []string + }{ + {[]geo.Point{ + {Lon: -80.881, Lat: 35.282}, + {Lon: -80.858, Lat: 35.281}, + {Lon: -80.864, Lat: 35.270}, + }, "loc", []string{"k", "l"}}, + {[]geo.Point{ + {Lon: -82.467, Lat: 36.356}, + {Lon: -78.127, Lat: 36.321}, + {Lon: -80.555, Lat: 32.932}, + {Lon: -84.807, Lat: 33.111}, + }, "loc", []string{"k", "l", "m"}}, + // same polygon vertices + {[]geo.Point{{Lon: -82.467, Lat: 36.356}, {Lon: -82.467, Lat: 36.356}, {Lon: -82.467, Lat: 36.356}, {Lon: -82.467, Lat: 36.356}}, "loc", nil}, + // non-overlaping polygon + {[]geo.Point{{Lon: -89.113, Lat: 36.400}, {Lon: -93.947, Lat: 36.471}, {Lon: -93.947, Lat: 34.031}}, "loc", nil}, + // concave polygon with a document `n` residing inside the hands, but outside the polygon + {[]geo.Point{{Lon: -71.65, Lat: 42.446}, {Lon: -71.649, Lat: 42.428}, {Lon: -71.640, Lat: 42.445}, {Lon: -71.649, Lat: 42.435}}, "loc", nil}, + // V like concave polygon with a document 'p' residing inside the bottom corner + {[]geo.Point{{Lon: -80.304, Lat: 40.740}, {Lon: -80.038, Lat: 40.239}, {Lon: -79.562, Lat: 40.786}, {Lon: -80.018, Lat: 40.328}}, "loc", []string{"p"}}, + {[]geo.Point{ + {Lon: -111.918, Lat: 33.515}, + {Lon: -111.938, Lat: 33.494}, + {Lon: -111.944, Lat: 33.481}, + {Lon: -111.886, Lat: 33.517}, + {Lon: -111.919, Lat: 33.468}, + {Lon: -111.929, Lat: 33.508}, + }, "loc", []string{"q"}}, + // real points near cb bangalore + {[]geo.Point{ + {Lat: 12.974872, Lon: 77.607749}, + {Lat: 12.971725, Lon: 77.610110}, + {Lat: 12.972530, Lon: 77.606912}, + {Lat: 12.975112, Lon: 77.603780}, + }, "loc", []string{"amoeba", "communiti"}}, + } + + i := setupGeoPolygonPoints(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for _, test := range tests { + got, err := testGeoPolygonSearch(indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("expected %v, got %v for polygon: %+v", test.want, got, test.polygon) + } + } +} + +func TestGeoRectanglePolygon(t *testing.T) { + tests := []struct { + polygon []geo.Point + field string + want []string + }{ + { + []geo.Point{{Lon: 0, Lat: 0}, {Lon: 0, Lat: 50}, {Lon: 50, Lat: 50}, {Lon: 50, Lat: 0}, {Lon: 0, Lat: 0}}, + "loc", + []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + }, + } + + i := setupGeo(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for _, test := range tests { + got, err := testGeoPolygonSearch(indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("expected %v, got %v for polygon: %+v", test.want, got, test.polygon) + } + } +} + +func testGeoPolygonSearch(i index.IndexReader, polygon []geo.Point, field string) ([]string, error) { + var rv []string + gbs, err := NewGeoBoundedPolygonSearcher(context.TODO(), i, polygon, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + rv = append(rv, string(docMatch.IndexInternalID)) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeoPolygonPoints(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := upsidedown.NewUpsideDownCouch( + gtreap.Name, + map[string]interface{}{ + "path": "", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + doc := document.NewDocument("k") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, -80.86469327, 35.2782)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("l") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, -80.8713, 35.28138)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("m") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, -84.25, 33.153)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("n") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, -89.992, 35.063)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("o") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, -71.648, 42.437)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("p") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, -80.016, 40.314)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("q") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, -111.919, 33.494)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("r") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 1.5, 1.1)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("s") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 2, 1.5)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("t") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 2.0, 1.9)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("u") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 2.0, 1.0)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("amoeba") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 77.60490, 12.97467)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("communiti") + doc.AddField(document.NewGeoPointField("loc", []uint64{}, 77.608237, 12.97237)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} + +type geoPoint struct { + title string + lon float64 + lat float64 +} + +// Test points inside a complex self intersecting polygon +func TestComplexGeoPolygons(t *testing.T) { + tests := []struct { + polygon []geo.Point + points []geoPoint + field string + want []string + }{ + /* + /\ /\ + /__\____/__\ + \ / + \/ + */ + // a, b, c - inside and d - on vertices. + { + []geo.Point{ + {Lon: 6.0, Lat: 2.0}, + {Lon: 3.0, Lat: 4.0}, + {Lon: 9.0, Lat: 6.0}, + {Lon: 3.0, Lat: 8.0}, + {Lon: 6.0, Lat: 10.0}, + {Lon: 6.0, Lat: 2.0}, + }, + []geoPoint{ + {title: "a", lon: 3, lat: 4}, + {title: "b", lon: 7, lat: 6}, + {title: "c", lon: 4, lat: 8.1}, + {title: "d", lon: 6, lat: 10.0}, + {title: "e", lon: 5, lat: 6}, + {title: "f", lon: 7, lat: 5}, + }, + "loc", + []string{"a", "b", "c", "d"}, + }, + /* + ____ + \ / + \/ + /\ + /__\ + */ + { + []geo.Point{ + {Lon: 7.0, Lat: 2.0}, + {Lon: 1.0, Lat: 8.0}, + {Lon: 1.0, Lat: 2.0}, + {Lon: 7.0, Lat: 8.0}, + {Lon: 7.0, Lat: 2.0}, + }, + []geoPoint{ + {title: "a", lon: 6, lat: 5}, + {title: "b", lon: 5, lat: 5}, + {title: "c", lon: 3, lat: 5.0}, + {title: "d", lon: 2, lat: 4.0}, + {title: "e", lon: 5, lat: 3}, + {title: "f", lon: 4, lat: 4}, + }, + "loc", + []string{"a", "b", "c", "d"}, + }, + } + + for _, test := range tests { + i := setupComplexGeoPolygonPoints(t, test.points) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + got, err := testGeoPolygonSearch(indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("expected %v, got %v for polygon: %+v", test.want, got, test.polygon) + } + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + } +} + +func setupComplexGeoPolygonPoints(t *testing.T, points []geoPoint) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := upsidedown.NewUpsideDownCouch( + gtreap.Name, + map[string]interface{}{ + "path": "", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + for _, point := range points { + doc := document.NewDocument(point.title) + doc.AddField(document.NewGeoPointField("loc", []uint64{}, point.lon, point.lat)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + } + return i +} diff --git a/search/searcher/search_geoshape.go b/search/searcher/search_geoshape.go new file mode 100644 index 0000000..6cd0977 --- /dev/null +++ b/search/searcher/search_geoshape.go @@ -0,0 +1,135 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "bytes" + "context" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" + "github.com/blevesearch/geo/geojson" + "github.com/blevesearch/geo/s2" +) + +func NewGeoShapeSearcher(ctx context.Context, indexReader index.IndexReader, shape index.GeoJSON, + relation string, field string, boost float64, + options search.SearcherOptions, +) (search.Searcher, error) { + var err error + var spatialPlugin index.SpatialAnalyzerPlugin + + // check for the spatial plugin from the index. + if sr, ok := indexReader.(index.SpatialIndexPlugin); ok { + spatialPlugin, _ = sr.GetSpatialAnalyzerPlugin("s2") + } + + if spatialPlugin == nil { + // fallback to the default spatial plugin(s2). + spatialPlugin = geo.GetSpatialAnalyzerPlugin("s2") + } + + // obtain the query tokens. + terms := spatialPlugin.GetQueryTokens(shape) + mSearcher, err := NewMultiTermSearcher(ctx, indexReader, terms, + field, boost, options, false) + if err != nil { + return nil, err + } + + dvReader, err := indexReader.DocValueReader([]string{field}) + if err != nil { + return nil, err + } + + return NewFilteringSearcher(ctx, mSearcher, buildRelationFilterOnShapes(ctx, dvReader, field, relation, shape)), nil +} + +// Using the same term splitter slice used in the doc values in zap. +// TODO: This needs to be revisited whenever we change the zap +// implementation of doc values. +var termSeparatorSplitSlice = []byte{0xff} + +func buildRelationFilterOnShapes(ctx context.Context, dvReader index.DocValueReader, field string, + relation string, shape index.GeoJSON, +) FilterFunc { + // this is for accumulating the shape's actual complete value + // spread across multiple docvalue visitor callbacks. + var dvShapeValue []byte + var startReading, finishReading bool + var reader *bytes.Reader + + var bufPool *s2.GeoBufferPool + if bufPoolCallback, ok := ctx.Value(search.GeoBufferPoolCallbackKey).(search.GeoBufferPoolCallbackFunc); ok { + bufPool = bufPoolCallback() + } + + return func(d *search.DocumentMatch) bool { + var found bool + + err := dvReader.VisitDocValues(d.IndexInternalID, + func(field string, term []byte) { + // only consider the values which are GlueBytes prefixed or + // if it had already started reading the shape bytes from previous callbacks. + if startReading || len(term) > geo.GlueBytesOffset { + + if !startReading && bytes.Equal(geo.GlueBytes, term[:geo.GlueBytesOffset]) { + startReading = true + + if bytes.Equal(geo.GlueBytes, term[len(term)-geo.GlueBytesOffset:]) { + term = term[:len(term)-geo.GlueBytesOffset] + finishReading = true + } + + dvShapeValue = append(dvShapeValue, term[geo.GlueBytesOffset:]...) + + } else if startReading && !finishReading { + if len(term) > geo.GlueBytesOffset && + bytes.Equal(geo.GlueBytes, term[len(term)-geo.GlueBytesOffset:]) { + term = term[:len(term)-geo.GlueBytesOffset] + finishReading = true + } + + term = append(termSeparatorSplitSlice, term...) + dvShapeValue = append(dvShapeValue, term...) + } + + // apply the filter once the entire docvalue is finished reading. + if finishReading { + v, err := geojson.FilterGeoShapesOnRelation(shape, dvShapeValue, relation, &reader, bufPool) + if err == nil && v { + found = true + } + + dvShapeValue = dvShapeValue[:0] + startReading = false + finishReading = false + } + } + }) + + if err == nil && found { + bytes := dvReader.BytesRead() + if bytes > 0 { + reportIOStats(ctx, bytes) + search.RecordSearchCost(ctx, search.AddM, bytes) + } + return found + } + + return false + } +} diff --git a/search/searcher/search_geoshape_circle_test.go b/search/searcher/search_geoshape_circle_test.go new file mode 100644 index 0000000..b4e1fb2 --- /dev/null +++ b/search/searcher/search_geoshape_circle_test.go @@ -0,0 +1,541 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoJsonCircleIntersectsQuery(t *testing.T) { + tests := []struct { + centrePoint []float64 + radiusInMeters string + field string + want []string + }{ + // test intersecting query circle for polygon1. + { + []float64{77.68115043640137, 12.94663769274367}, + "200m", + "geometry", + []string{"polygon1"}, + }, + + // test intersecting query circle for polygon1, circle1 and linestring1. + { + []float64{77.68115043640137, 12.94663769274367}, + "750m", + "geometry", + []string{"polygon1", "circle1", "linestring1"}, + }, + + // test intersecting query circle for linestring2. + { + []float64{77.69591331481932, 12.92756503709986}, + "250m", + "geometry", + []string{"linestring2"}, + }, + + // test intersecting query circle for circle1. + {[]float64{77.6767, 12.9422}, "250m", "geometry", []string{"circle1"}}, + + // test intersecting query circle for point1, envelope1 and linestring3. + { + []float64{81.243896484375, 26.22444694563432}, + "90000m", + "geometry", + []string{"point1", "envelope1", "linestring3"}, + }, + + // test intersecting query circle for envelope. + { + []float64{79.98458862304688, 25.339061458818374}, + "1250m", + "geometry", + []string{"envelope1"}, + }, + + // test intersecting query circle for multipoint. + { + []float64{81.87346458435059, 25.41505910223247}, + "200m", + "geometry", + []string{"multipoint1"}, + }, + + // test intersecting query circle for multilinestring. + { + []float64{81.8669843673706, 25.512661276952272}, + "90m", + "geometry", + []string{"multilinestring1"}, + }, + } + + i := setupGeoJsonShapesIndexForCircleQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeCircleRelationQuery("intersects", + indexReader, test.centrePoint, test.radiusInMeters, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.centrePoint) + } + } +} + +func TestGeoJsonCircleWithInQuery(t *testing.T) { + tests := []struct { + centrePoint []float64 + radiusInMeters string + field string + want []string + }{ + // test query circle containing polygon2 and multilinestring2. + { + []float64{81.85981750488281, 25.546778150624146}, + "3700m", + "geometry", + []string{"polygon2", "multilinestring2"}, + }, + + // test query circle containing multilinestring2. + { + []float64{81.85981750488281, 25.546778150624146}, + "3250m", + "geometry", + []string{"multilinestring2"}, + }, + + // test query circle containing multipoint1. + { + []float64{81.88599586486816, 25.425756968727935}, + "1650m", + "geometry", + []string{"multipoint1"}, + }, + + // test query circle containing circle2. + { + []float64{82.09362030029297, 25.546313513788725}, + "1280m", + "geometry", + []string{"envelope2", "circle2"}, + }, + + // test query circle containing envelope2 and circle2. + { + []float64{82.10289001464844, 25.544919592476727}, + "700m", + "geometry", + []string{"envelope2", "circle2"}, + }, + + // test query circle containing point1 and linestring3. + { + []float64{81.27685546875, 26.1899475672235}, + "5600m", + "geometry", + []string{"point1", "linestring3"}, + }, + } + + i := setupGeoJsonShapesIndexForCircleQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeCircleRelationQuery("within", indexReader, test.centrePoint, test.radiusInMeters, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", n, test.want, got, test.centrePoint) + } + } +} + +func TestGeoJsonCircleContainsQuery(t *testing.T) { + tests := []struct { + centrePoint []float64 + radiusInMeters string + field string + want []string + }{ + // test query circle within polygon3. + { + []float64{8.549551963806152, 47.3759038562437}, + "180m", + "geometry", + []string{"polygon3"}, + }, + + // test query circle containing envelope3. + { + []float64{8.551011085510254, 47.380117626829275}, + "75m", + "geometry", + []string{"envelope3"}, + }, + + // test query circle exceeding envelope3 with a few meters. + { + []float64{8.551011085510254, 47.380117626829275}, + "78m", + "geometry", nil, + }, + + // test query circle containing circle3. + { + []float64{8.535819053649902, 47.38297989270074}, + "185m", + "geometry", + []string{"circle3"}, + }, + } + + i := setupGeoJsonShapesIndexForCircleQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeCircleRelationQuery("contains", + indexReader, test.centrePoint, test.radiusInMeters, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.centrePoint) + } + } +} + +func runGeoShapeCircleRelationQuery(relation string, i index.IndexReader, + points []float64, radius string, field string, +) ([]string, error) { + var rv []string + s := geo.NewGeoCircle(points, radius) + + gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + docID, _ := i.ExternalID(docMatch.IndexInternalID) + rv = append(rv, docID) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeoJsonShapesIndexForCircleQuery(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + polygon1 := [][][][]float64{{{ + {77.67248153686523, 12.957679089615821}, + {77.67956256866455, 12.948101542434257}, + {77.68908977508545, 12.948896200093982}, + {77.68934726715086, 12.955211547173878}, + {77.68016338348389, 12.954291440344619}, + {77.67248153686523, 12.957679089615821}, + }}} + doc := document.NewDocument("polygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + polygon2 := [][][][]float64{{{ + {81.84951782226561, 25.522692102524033}, + {81.8557834625244, 25.521762640415535}, + {81.86264991760254, 25.521762640415535}, + {81.86676979064941, 25.521607729364224}, + {81.89560890197754, 25.542673796271302}, + {81.88977241516113, 25.543293330460937}, + {81.84951782226561, 25.522692102524033}, + }}} + doc = document.NewDocument("polygon2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon2, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon3 := [][][][]float64{{{ + {8.548071384429932, 47.379216780040124}, + {8.547642230987549, 47.3771680227784}, + {8.545818328857422, 47.37677569847655}, + {8.546290397644043, 47.37417465983494}, + {8.551719188690186, 47.37417465983494}, + {8.553242683410645, 47.37679022905829}, + {8.548071384429932, 47.379216780040124}, + }}} + doc = document.NewDocument("polygon3") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon3, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + point1 := [][][][]float64{{{{81.2439, 26.2244}}}} + doc = document.NewDocument("point1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + point1, "point", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + envelope1 := [][][][]float64{{{ + {79.9969482421875, 23.895882703682627}, + {80.7220458984375, 25.750424835909385}, + }}} + doc = document.NewDocument("envelope1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + envelope1, "envelope", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + envelope2 := [][][][]float64{{{ + {82.10409164428711, 25.54360309635522}, + {82.10537910461424, 25.544609829984058}, + }}} + doc = document.NewDocument("envelope2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + envelope2, "envelope", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + envelope3 := [][][][]float64{{{ + {8.545668125152588, 47.37942019840244}, + {8.552148342132568, 47.383778974713124}, + }}} + doc = document.NewDocument("envelope3") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + envelope3, "envelope", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle1") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{}, + []float64{77.67252445220947, 12.936348678099293}, "900m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle2") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{}, + []float64{82.10289001464844, 25.544919592476727}, "100m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle3") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{}, + []float64{ + 8.53363037109375, + 47.38191927423153, + }, "400m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring := [][][][]float64{{{ + {77.68715858459473, 12.944755587650944}, + {77.69213676452637, 12.945090185150542}, + }}} + doc = document.NewDocument("linestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring1 := [][][][]float64{{{ + {77.68913269042969, 12.929614580987227}, + {77.70252227783203, 12.929698235482276}, + }}} + doc = document.NewDocument("linestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring1, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring2 := [][][][]float64{{{ + {81.26792907714844, 26.170845301716813}, + {81.30157470703125, 26.18440207077121}, + }}} + doc = document.NewDocument("linestring3") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring2, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring := [][][][]float64{{{ + {81.86170578002928, 25.430407918899984}, + {81.86273574829102, 25.421958559611397}, + }, { + {81.88230514526367, 25.437616536907512}, + {81.90084457397461, 25.431415601111418}, + }, { + {81.86805725097656, 25.514868905100244}, + {81.86702728271484, 25.502474677473746}, + }}} + doc = document.NewDocument("multilinestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring1 := [][][][]float64{{ + { + {81.84642791748047, 25.561335859046192}, + {81.84230804443358, 25.550495180470026}, + }, + {{81.87423706054688, 25.55142441992021}, {81.88453674316406, 25.555141305670045}}, + {{81.8642807006836, 25.572175556682115}, {81.87458038330078, 25.567839795359724}}, + }} + doc = document.NewDocument("multilinestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring1, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipoint1 := [][][][]float64{{{ + {81.87337875366211, 25.432268248708212}, + {81.87355041503906, 25.416299483230368}, + {81.90118789672852, 25.426067037656946}, + }}} + doc = document.NewDocument("multipoint1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipoint1, "multipoint", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygonWithHole1 := [][][][]float64{{ + { + {77.59991168975829, 12.972232910164502}, + {77.6039457321167, 12.97582941279006}, + {77.60424613952637, 12.98168407323241}, + {77.59974002838135, 12.985489528568463}, + {77.59321689605713, 12.979300406693417}, + {77.59991168975829, 12.972232910164502}, + }, + { + {77.59682178497314, 12.975787593290978}, + {77.60295867919922, 12.975787593290978}, + {77.60295867919922, 12.98143316204164}, + {77.59682178497314, 12.98143316204164}, + {77.59682178497314, 12.975787593290978}, + }, + }} + + doc = document.NewDocument("polygonWithHole1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygonWithHole1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} diff --git a/search/searcher/search_geoshape_envelope_test.go b/search/searcher/search_geoshape_envelope_test.go new file mode 100644 index 0000000..580fd54 --- /dev/null +++ b/search/searcher/search_geoshape_envelope_test.go @@ -0,0 +1,520 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoJsonEnvelopeWithInQuery(t *testing.T) { + tests := []struct { + points [][]float64 + field string + want []string + }{ + // test within query envelope for point1. + { + [][]float64{ + {76.256103515625, 16.76772739719064}, + {76.35772705078125, 16.872890378907783}, + }, + "geometry", + []string{"point1"}, + }, + + // test within query envelope for multipoint1. + { + [][]float64{ + {81.046142578125, 17.156537255486093}, + {81.331787109375, 17.96305758238804}, + }, + "geometry", + []string{"multipoint1"}, + }, + + // test within query envelope for partial points in a multipoint1. + { + [][]float64{ + {81.05987548828125, 17.16178591271515}, + {81.36199951171875, 17.861132899477624}, + }, + "geometry", nil, + }, + + // test within query envelope for polygon2 and point1. + { + [][]float64{ + {76.00341796875, 16.573022719182777}, + {76.717529296875, 17.006888277600524}, + }, + "geometry", + []string{"polygon2", "point1"}, + }, + + // test within query envelope for linestring1. + { + [][]float64{ + {76.84112548828125, 16.86500518090961}, + {77.62115478515625, 17.531439701706244}, + }, + "geometry", + []string{"linestring1"}, + }, + + // test within query envelope for multilinestring1. + { + [][]float64{ + {81.683349609375, 17.104042525557904}, + {81.99234008789062, 17.66495983051931}, + }, + "geometry", + []string{"multilinestring1"}, + }, + + // test within query envelope that is intersecting multilinestring1. + { + [][]float64{ + {81.65725708007812, 17.2601707001208}, + {81.95114135742186, 17.66495983051931}, + }, + "geometry", nil, + }, + + // test within query envelope for envelope1 and circle1. + { + [][]float64{ + {74.75372314453125, 17.36636733709516}, + {75.509033203125, 18.038809662036805}, + }, + "geometry", + []string{"envelope1", "circle1"}, + }, + + // test within query envelope for envelope1. + { + [][]float64{ + {74.783935546875, 17.38209494787749}, + {75.96221923828125, 17.727758609852284}, + }, + "geometry", + []string{"envelope1"}, + }, + } + i := setupGeoJsonShapesIndexForEnvelopeQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeEnvelopeRelationQuery("within", + indexReader, test.points, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.points) + } + } +} + +func TestGeoJsonEnvelopeIntersectsQuery(t *testing.T) { + tests := []struct { + points [][]float64 + field string + want []string + }{ + // test intersecting query envelope for partial points in a multipoint1. + { + [][]float64{ + {81.00769042968749, 17.80622614478282}, + {81.199951171875, 17.983957957423037}, + }, + "geometry", + []string{"multipoint1"}, + }, + + // test intersecting query envelope that is intersecting multilinestring1. + { + [][]float64{ + {81.65725708007812, 17.2601707001208}, + {81.95114135742186, 17.66495983051931}, + }, + "geometry", + []string{"multilinestring1"}, + }, + + // test intersecting query envelope for linestring2. + { + [][]float64{ + {81.9854736328125, 18.27369419984127}, + {82.14752197265625, 18.633232565431218}, + }, + "geometry", + []string{"linestring2"}, + }, + + // test intersecting query envelope for circle2. + { + [][]float64{ + {82.6336669921875, 17.82714499951342}, + {82.66387939453125, 17.861132899477624}, + }, + "geometry", + []string{"circle2"}, + }, + + // test intersecting query envelope for polygon3. + { + [][]float64{ + {82.92343139648438, 17.739530934289657}, + {82.98797607421874, 17.79184300887134}, + }, + "geometry", + []string{"polygon3"}, + }, + } + i := setupGeoJsonShapesIndexForEnvelopeQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeEnvelopeRelationQuery("intersects", indexReader, test.points, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", n, test.want, got, test.points) + } + } +} + +func TestGeoJsonEnvelopeContainsQuery(t *testing.T) { + tests := []struct { + points [][]float64 + field string + want []string + }{ + // test envelope contained within polygon1. + { + [][]float64{ + {8.548285961151123, 47.376092756617446}, + {8.551225662231445, 47.37764752629426}, + }, + "geometry", + []string{"polygon1"}, + }, + + // test envelope partially contained within polygon1. + { + [][]float64{ + {8.549273014068604, 47.376194471922986}, + {8.551654815673828, 47.37827232736301}, + }, + "geometry", nil, + }, + + // test envelope partially contained within polygon1. + { + [][]float64{ + {8.549273014068604, 47.376194471922986}, + {8.551654815673828, 47.37827232736301}, + }, + "geometry", nil, + }, + + // test envelope fully contained within circle3. + { + [][]float64{ + {8.532772064208984, 47.380379160110856}, + {8.534531593322752, 47.38299442157271}, + }, + "geometry", + []string{"circle3"}, + }, + + // test envelope partially contained within circle3. + { + [][]float64{ + {8.532836437225342, 47.38010309716447}, + {8.538415431976318, 47.383081594720466}, + }, + "geometry", nil, + }, + } + i := setupGeoJsonShapesIndexForEnvelopeQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeEnvelopeRelationQuery("contains", + indexReader, test.points, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.points) + } + } +} + +func runGeoShapeEnvelopeRelationQuery(relation string, i index.IndexReader, + points [][]float64, field string, +) ([]string, error) { + var rv []string + s := geo.NewGeoEnvelope(points) + + gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + docID, _ := i.ExternalID(docMatch.IndexInternalID) + rv = append(rv, docID) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeoJsonShapesIndexForEnvelopeQuery(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + polygon1 := [][][][]float64{{{ + {8.548071384429932, 47.379216780040124}, + {8.547642230987549, 47.3771680227784}, + {8.545818328857422, 47.37677569847655}, + {8.546290397644043, 47.37417465983494}, + {8.551719188690186, 47.37417465983494}, + {8.553242683410645, 47.37679022905829}, + {8.548071384429932, 47.379216780040124}, + }}} + doc := document.NewDocument("polygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon2 := [][][][]float64{{{ + {76.70379638671874, 16.828203242420393}, + {76.36322021484375, 16.58881695544584}, + {76.70928955078125, 16.720385051694}, + {76.70379638671874, 16.828203242420393}, + }}} + doc = document.NewDocument("polygon2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon2, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon3 := [][][][]float64{{{ + {82.9522705078125, 17.749994573141873}, + {82.94952392578125, 17.692436998627272}, + {82.87673950195312, 17.64009591883757}, + {82.76412963867188, 17.58643052828743}, + {82.8094482421875, 17.522272941245202}, + {82.99621582031249, 17.64009591883757}, + {82.9522705078125, 17.749994573141873}, + }}} + doc = document.NewDocument("polygon3") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon3, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + envelope1 := [][][][]float64{{{ + {74.89654541015625, 17.403062993328923}, + {74.92401123046875, 17.66495983051931}, + }}} + doc = document.NewDocument("envelope1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + envelope1, "envelope", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle1") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{}, + []float64{75.0531005859375, 17.675427818339383}, "12900m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle2") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{}, + []float64{82.69683837890625, 17.902955242676995}, "6000m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle3") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{}, + []float64{ + 8.53363037109375, + 47.38191927423153, + }, "400m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + point1 := [][][][]float64{{{{76.29730224609375, 16.796653031618053}}}} + doc = document.NewDocument("point1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + point1, "point", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring1 := [][][][]float64{{{ + {76.85211181640624, 17.51048642597462}, + {77.24212646484374, 16.93070509876554}, + }}} + doc = document.NewDocument("linestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring1, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring2 := [][][][]float64{{{ + {81.89208984375, 18.555136195095105}, + {82.21343994140625, 18.059701055000478}, + }}} + doc = document.NewDocument("linestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring2, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipoint1 := [][][][]float64{{{ + {81.24938964843749, 17.602139123350838}, + {81.30432128906249, 17.56548361143177}, + {81.29058837890625, 17.180155043474496}, + {81.09283447265625, 17.87681743233167}, + }}} + doc = document.NewDocument("multipoint1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipoint1, "multipoint", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring := [][][][]float64{{ + { + {81.69708251953125, 17.641404631355755}, + {81.90994262695312, 17.642713334367667}, + }, + {{81.6998291015625, 17.620464090732245}, {81.69708251953125, 17.468572623463153}}, + {{81.70120239257811, 17.458092664041494}, {81.81243896484375, 17.311310073048123}}, + {{81.815185546875, 17.3034434020238}, {81.81243896484375, 17.109292665395643}}, + }} + doc = document.NewDocument("multilinestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring1 := [][][][]float64{{ + { + {77.6015853881836, 12.990089451715061}, + {77.60476112365723, 12.987747683302153}, + }, + {{77.59875297546387, 12.988751301039581}, {77.59446144104004, 12.98197680263484}}, + {{77.60188579559325, 12.982604078764705}, {77.60557651519775, 12.987329508048184}}, + }} + doc = document.NewDocument("multilinestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring1, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} diff --git a/search/searcher/search_geoshape_geometrycollection_test.go b/search/searcher/search_geoshape_geometrycollection_test.go new file mode 100644 index 0000000..f9dd312 --- /dev/null +++ b/search/searcher/search_geoshape_geometrycollection_test.go @@ -0,0 +1,692 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoJSONIntersectsQueryAgainstGeometryCollection(t *testing.T) { + tests := []struct { + points [][][][][]float64 + types []string + field string + want []string + }{ + // test intersects geometrycollection query for gc_polygon1_linestring1. + { + [][][][][]float64{ + {{{ + {-120.80017089843749, 36.54053616262899}, + {-120.67932128906249, 36.33725319397006}, + {-120.30578613281251, 36.90597988519294}, + {-120.80017089843749, 36.54053616262899}, + }}}, + {{{{-118.24584960937499, 35.32184842037683}, {-117.8668212890625, 35.06597313798418}}}}, + }, + []string{"polygon", "linestring"}, + "geometry", + []string{"gc_polygon1_linestring1"}, + }, + + // test intersects geometrycollection query for gc_polygon1_linestring1. + { + [][][][][]float64{ + {{ + {{-118.3172607421875, 35.250105158539355}, {-117.50976562499999, 35.37561413174875}}, + {{-118.69628906249999, 34.6241677899049}, {-118.3172607421875, 35.03899204678081}}, + {{-117.94921874999999, 35.146862906756304}, {-117.674560546875, 34.41144164327245}}, + }}, + {{{ + {-117.04284667968749, 35.263561862152095}, + {-116.8505859375, 35.263561862152095}, + {-116.8505859375, 35.33529320309328}, + {-117.04284667968749, 35.33529320309328}, + {-117.04284667968749, 35.263561862152095}, + }}}, + }, + []string{"multilinestring", "polygon"}, + "geometry", + []string{"gc_polygon1_linestring1"}, + }, + + // test intersects geometrycollection query for gc_multipolygon1_multilinestring1. + { + [][][][][]float64{ + {{ + {{-115.8563232421875, 38.53957267203905}, {-115.58166503906251, 38.54816542304656}}, + {{-115.8343505859375, 38.45789034424927}, {-115.81237792968749, 38.19502155795575}}, + }}, + {{{{-116.64905548095702, 37.94920616351679}}}}, + }, + []string{"multilinestring", "point"}, + "geometry", + []string{"gc_multipolygon1_multilinestring1"}, + }, + + // test intersects geometrycollection query for gc_polygon1_linestring1 and gc_multipolygon1_multilinestring1. + { + [][][][][]float64{ + {{{{-116.64905548095702, 37.94920616351679}, {-118.29528808593751, 34.52466147177172}}}}, + {{ + {{-115.8563232421875, 38.53957267203905}, {-115.58166503906251, 38.54816542304656}}, + {{-115.8343505859375, 38.45789034424927}, {-115.81237792968749, 38.19502155795575}}, + }}, + }, + []string{"multipoint", "multilinestring"}, + "geometry", + []string{ + "gc_polygon1_linestring1", + "gc_multipolygon1_multilinestring1", + }, + }, + + // test intersects geometrycollection query for gc_polygon1_linestring1 and gc_multipolygon1_multilinestring1. + { + [][][][][]float64{ + {{{ + {-117.46582031249999, 36.146746777814364}, + {-116.70227050781249, 36.146746777814364}, + {-116.70227050781249, 36.69485094156225}, + {-117.46582031249999, 36.69485094156225}, + {-117.46582031249999, 36.146746777814364}, + }}, {{ + {-115.5267333984375, 38.06106741381201}, + {-115.4937744140625, 37.18220222107978}, + {-114.93896484374999, 37.304644804751106}, + {-115.5267333984375, 38.06106741381201}, + }}}, + {{ + {{-115.8563232421875, 38.53957267203905}, {-115.58166503906251, 38.54816542304656}}, + {{-115.8343505859375, 38.45789034424927}, {-115.81237792968749, 38.19502155795575}}, + }}, + }, + []string{"multipolygon", "multilinestring"}, + "geometry", + []string{"gc_point1_multipoint1"}, + }, + } + i := setupGeoJsonShapesIndexForGeometryCollectionQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeGeometryCollectionRelationQuery("intersects", + indexReader, test.points, test.types, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.points) + } + } +} + +func TestGeoJSONWithInQueryAgainstGeometryCollection(t *testing.T) { + tests := []struct { + points [][][][][]float64 + types []string + field string + want []string + }{ + // test within geometrycollection query for gc_multipoint2_multipolygon2_multiline2. + { + [][][][][]float64{ + {{{{-122.40434646606444, 37.73400071182758}, {-122.39730834960938, 37.73691949864062}}}}, + {{{ + {-122.42511749267578, 37.760808496517235}, + {-122.42314338684082, 37.74248523826606}, + {-122.40082740783691, 37.756669194195815}, + {-122.42511749267578, 37.760808496517235}, + }}}, + { + {{ + {-122.46339797973633, 37.76637243960179}, + {-122.46176719665527, 37.7502901437285}, + {-122.43644714355469, 37.75911208915015}, + {-122.46339797973633, 37.76637243960179}, + }}, + {{ + {-122.43653297424315, 37.714720253587004}, + {-122.40563392639159, 37.714720253587004}, + {-122.40563392639159, 37.72904529863455}, + {-122.43653297424315, 37.72904529863455}, + {-122.43653297424315, 37.714720253587004}, + }}, + }, + }, + []string{"linestring", "polygon", "multipolygon"}, + "geometry", + []string{"gc_multipoint2_multipolygon2_multiline2"}, + }, + + // test within geometrycollection query. + { + [][][][][]float64{ + {{{{-122.40434646606444, 37.73400071182758}, {-122.39730834960938, 37.73691949864062}}}}, + { + {{ + {-122.46339797973633, 37.76637243960179}, + {-122.46176719665527, 37.7502901437285}, + {-122.43644714355469, 37.75911208915015}, + {-122.46339797973633, 37.76637243960179}, + }}, + {{ + {-122.43653297424315, 37.714720253587004}, + {-122.40563392639159, 37.714720253587004}, + {-122.40563392639159, 37.72904529863455}, + {-122.43653297424315, 37.72904529863455}, + {-122.43653297424315, 37.714720253587004}, + }}, + }, + }, + []string{"linestring", "multipolygon"}, + "geometry", nil, + }, + + // test within geometrycollection for gc_multipoint2_multipolygon2_multiline2. + { + [][][][][]float64{ + {{{ + {-122.4491500854492, 37.78170504295941}, + {-122.4862289428711, 37.747371884118664}, + {-122.43078231811525, 37.6949593672454}, + {-122.3799705505371, 37.72945260537779}, + {-122.3928451538086, 37.78007695280165}, + {-122.4491500854492, 37.78170504295941}, + }}}, + }, + []string{"polygon"}, + "geometry", + []string{"gc_multipoint2_multipolygon2_multiline2"}, + }, + + // test within geometrycollection for gc_multipolygon3 + // gc_multipolygon3's multipolygons within the geometrycollection is covered by the + // query's geometric collection of a polygon and a multipolygon. + { + [][][][][]float64{ + {{{ + {86.6162109375, 57.26716357153586}, + {85.1220703125, 8119}, + {84.462890625, 56.27996083172844}, + {86.98974609375, 55.70235509327093}, + {87.802734375, 56.77680831656842}, + {86.6162109375, 57.26716357153586}, + }}}, + { + {{ + {75.1025390625, 54.3549556895541}, + {73.1689453125, 54.29088164657006}, + {72.7294921875, 53.08082737207479}, + {74.091796875, 51.998410382390325}, + {76.79443359375, 53.396432127095984}, + {75.1025390625, 54.3549556895541}, + }}, + {{ + {80.1123046875, 55.57834467218206}, + {78.9697265625, 55.65279803318956}, + {78.5302734375, 54.635697306063854}, + {79.87060546875, 54.18815548107151}, + {80.96923828125, 54.80068486732233}, + {80.1123046875, 55.57834467218206}, + }}, + }, + }, + []string{"polygon", "multipolygon"}, + "geometry", + []string{"gc_multipolygon3"}, + }, + } + + i := setupGeoJsonShapesIndexForGeometryCollectionQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeGeometryCollectionRelationQuery("within", indexReader, test.points, test.types, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", n, test.want, got, test.points) + } + } +} + +func TestGeoJSONContainsQueryAgainstGeometryCollection(t *testing.T) { + tests := []struct { + points [][][][][]float64 + types []string + field string + want []string + }{ + // test contains for a geometrycollection that comprises of a linestring, + // polygon, multipolygon, point and multipoint for polygon2. + { + [][][][][]float64{ + // linestring + {{{{7.457013130187988, 46.966401589723894}, {7.482891082763671, 46.94554547022893}}}}, + // polygon + {{{ + {7.466454505920409, 46.965054389418476}, + {7.46143341064453, 46.9641171865865}, + {7.466325759887694, 46.96101258493027}, + {7.466454505920409, 46.965054389418476}, + }}}, + // multipolygon + { + {{ + {7.4811744689941415, 46.957966385567474}, + {7.478899955749511, 46.95492001277476}, + {7.484478950500488, 46.95509576976545}, + {7.4811744689941415, 46.957966385567474}, + }}, + {{ + {7.466540336608888, 46.94753769790697}, + {7.464609146118165, 46.946219320241674}, + {7.468342781066894, 46.94592634301753}, + {7.466540336608888, 46.94753769790697}, + }}, + {{ + {7.504348754882812, 47.00425575323296}, + {7.501087188720703, 47.001680295206874}, + {7.507266998291015, 47.00191443288521}, + {7.504348754882812, 47.00425575323296}, + }}, + }, + // point + {{{{7.449932098388673, 46.95817142366062}}}}, + // multipoint + {{{{7.479157447814942, 46.96370715518446}, {7.4532365798950195, 46.96657730900153}}}}, + }, + []string{"linestring", "polygon", "multipolygon", "point", "multipoint"}, + "geometry", + []string{"multipolygon4"}, + }, + + // test contains for a geometrycollection query with one point inside the multipoint lying outside + // polygon2. + { + [][][][][]float64{ + // linestring + {{{{7.457013130187988, 46.966401589723894}, {7.482891082763671, 46.94554547022893}}}}, + // polygon + {{{ + {7.466454505920409, 46.965054389418476}, + {7.46143341064453, 46.9641171865865}, + {7.466325759887694, 46.96101258493027}, + {7.466454505920409, 46.965054389418476}, + }}}, + // multipolygon + { + {{ + {7.4811744689941415, 46.957966385567474}, + {7.478899955749511, 46.95492001277476}, + {7.484478950500488, 46.95509576976545}, + {7.4811744689941415, 46.957966385567474}, + }}, + {{ + {7.466540336608888, 46.94753769790697}, + {7.464609146118165, 46.946219320241674}, + {7.468342781066894, 46.94592634301753}, + {7.466540336608888, 46.94753769790697}, + }}, + }, + // point + {{{{7.449932098388673, 46.95817142366062}}}}, + // multipoint + {{{{7.479157447814942, 46.96370715518446}, {7.475638389587402, 46.965200825877794}}}}, + }, + []string{"linestring", "polygon", "multipolygon", "point", "multipoint"}, + "geometry", + nil, + }, + + // test contains for a geometrycollection query with one point inside the multipoint lying outside + // polygon2. + { + [][][][][]float64{ + // linestring + {{{{7.457013130187988, 46.966401589723894}, {7.482891082763671, 46.94554547022893}}}}, + // polygon + {{{ + {7.466454505920409, 46.965054389418476}, + {7.46143341064453, 46.9641171865865}, + {7.466325759887694, 46.96101258493027}, + {7.466454505920409, 46.965054389418476}, + }}}, + // multipolygon + { + {{ + {7.4811744689941415, 46.957966385567474}, + {7.478899955749511, 46.95492001277476}, + {7.484478950500488, 46.95509576976545}, + {7.4811744689941415, 46.957966385567474}, + }}, + {{ + {7.466540336608888, 46.94753769790697}, + {7.464609146118165, 46.946219320241674}, + {7.468342781066894, 46.94592634301753}, + {7.466540336608888, 46.94753769790697}, + }}, + }, + // point + {{{{7.449932098388673, 46.95817142366062}}}}, + // multipoint + {{{{7.479157447814942, 46.96370715518446}, {7.4532365798950195, 46.96657730900153}}}}, + }, + []string{"linestring", "polygon", "multipolygon", "point", "multipoint"}, + "geometry", + []string{"polygon2", "multipolygon4"}, + }, + } + + i := setupGeoJsonShapesIndexForGeometryCollectionQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeGeometryCollectionRelationQuery("contains", + indexReader, test.points, test.types, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.points) + } + } +} + +func runGeoShapeGeometryCollectionRelationQuery(relation string, i index.IndexReader, + points [][][][][]float64, types []string, field string, +) ([]string, error) { + var rv []string + s, _, err := geo.NewGeometryCollection(points, types) + if err != nil { + return nil, err + } + + gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + docID, _ := i.ExternalID(docMatch.IndexInternalID) + rv = append(rv, docID) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeoJsonShapesIndexForGeometryCollectionQuery(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + // document gc_polygon1_linestring1 + polygon1 := [][][][]float64{{{ + {-118.15246582031249, 34.876918445772084}, + {-118.46557617187499, 34.773203753940734}, + {-118.3172607421875, 34.50655662164561}, + {-117.91625976562499, 34.4793919710481}, + {-117.76245117187499, 34.76417891445512}, + {-118.15246582031249, 34.876918445772084}, + }}} + + linestring1 := [][][][]float64{{{ + {-120.78918457031251, 36.87522650673951}, + {-118.9215087890625, 34.95349314197422}, + }}} + + coordinates := [][][][][]float64{polygon1, linestring1} + types := []string{"polygon", "linestring"} + + doc := document.NewDocument("gc_polygon1_linestring1") + doc.AddField(document.NewGeometryCollectionFieldWithIndexingOptions("geometry", + []uint64{}, coordinates, types, document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + // document gc_multipolygon1_multilinestring1 + multipolygon1 := [][][][]float64{ + {{ + {-117.24609374999999, 37.67512527892127}, + {-117.61962890624999, 37.26530995561875}, + {-116.597900390625, 37.56199695314352}, + {-117.24609374999999, 37.67512527892127}, + }}, + {{ + {-117.60864257812501, 38.71123253895224}, + {-117.41638183593749, 38.36750215395045}, + {-117.66357421875, 37.93986540897977}, + {-116.6473388671875, 37.94852933714952}, + {-117.1307373046875, 38.363195134453846}, + {-116.75170898437501, 38.7283759182398}, + {-117.60864257812501, 38.71123253895224}, + }}, + } + multilinestring1 := [][][][]float64{{ + {{-118.9215087890625, 38.74123075381228}, {-118.78967285156249, 38.43207668538207}}, + {{-118.57543945312501, 38.8225909761771}, {-118.45458984375, 38.522384090200845}}, + {{-118.94897460937499, 38.788345355085625}, {-118.61938476562499, 38.86965182408357}}, + }} + + coordinates = [][][][][]float64{multipolygon1, multilinestring1} + types = []string{"multipolygon", "multilinestring"} + doc = document.NewDocument("gc_multipolygon1_multilinestring1") + doc.AddField(document.NewGeometryCollectionFieldWithIndexingOptions("geometry", + []uint64{}, coordinates, types, document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + // document gc_point1_multipoint1 + point1 := [][][][]float64{{{{-115.10925292968749, 36.20882309283712}}}} + multipoint1 := [][][][]float64{{{ + {-117.13623046874999, 36.474306755095235}, + {-118.57543945312501, 36.518465989675875}, + {-118.58642578124999, 36.90597988519294}, + {-119.5477294921875, 37.85316995894978}, + }}} + + coordinates = [][][][][]float64{point1, multipoint1} + types = []string{"point", "multipoint"} + + doc = document.NewDocument("gc_point1_multipoint1") + doc.AddField(document.NewGeometryCollectionFieldWithIndexingOptions("geometry", + []uint64{}, coordinates, types, document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + // document gc_multipoint2_multipolygon2_multiline2 + multipoint2 := [][][][]float64{{{ + {-122.4052906036377, 37.75626203719391}, + {-122.42091178894044, 37.74757548736071}, + }}} + multipolygon2 := [][][][]float64{ + {{ + {-122.46168136596681, 37.765151122096945}, + {-122.46168136596681, 37.754972691904946}, + {-122.45103836059569, 37.754972691904946}, + {-122.451810836792, 37.7624370109886}, + {-122.46168136596681, 37.765151122096945}, + }}, + {{ + {-122.41902351379395, 37.726194088705576}, + {-122.43533134460448, 37.71668926284967}, + {-122.40777969360353, 37.71634978222733}, + {-122.41902351379395, 37.726194088705576}, + }}, + } + multilinestring2 := [][][][]float64{{ + {{-122.41284370422362, 37.73155698786267}, {-122.40700721740721, 37.73338978839743}}, + {{-122.40434646606444, 37.73400071182758}, {-122.39730834960938, 37.73691949864062}}, + }} + + coordinates = [][][][][]float64{multipoint2, multipolygon2, multilinestring2} + types = []string{"multipoint", "multipolygon", "multiline"} + + doc = document.NewDocument("gc_multipoint2_multipolygon2_multiline2") + doc.AddField(document.NewGeometryCollectionFieldWithIndexingOptions("geometry", + []uint64{}, coordinates, types, document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + // document gc_multipolygon3 + multipolygon3 := [][][][]float64{ + {{ + {85.60546875, 57.20771009775018}, + {86.396484375, 55.99838095535963}, + {87.03369140625, 56.71656572651468}, + {85.60546875, 57.20771009775018}, + }}, + {{ + {79.56298828125, 55.3915921070334}, + {79.60693359375, 54.43171285946844}, + {80.39794921875, 54.85131525968606}, + {79.56298828125, 55.3915921070334}, + }}, + {{ + {74.35546875, 54.13669645687002}, + {74.1796875, 52.802761415419674}, + {75.87158203125, 53.44880683542759}, + {74.35546875, 54.13669645687002}, + }}, + } + + coordinates = [][][][][]float64{multipolygon3} + types = []string{"multipolygon"} + + doc = document.NewDocument("gc_multipolygon3") + doc.AddField(document.NewGeometryCollectionFieldWithIndexingOptions("geometry", + []uint64{}, coordinates, types, document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon2 := [][][][]float64{{{ + {7.452635765075683, 46.96692874582506}, + {7.449803352355956, 46.95817142366062}, + {7.4573564529418945, 46.95149263607834}, + {7.462162971496582, 46.945955640812095}, + {7.483148574829102, 46.945311085627445}, + {7.487225532531738, 46.957029058564686}, + {7.4793291091918945, 46.96388288331302}, + {7.464480400085448, 46.96903731827891}, + {7.452635765075683, 46.96692874582506}, + }}} + + doc = document.NewDocument("polygon2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon2, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipolygon4 := [][][][]float64{ + {{ + {7.452635765075683, 46.96692874582506}, + {7.449803352355956, 46.95817142366062}, + {7.4573564529418945, 46.95149263607834}, + {7.462162971496582, 46.945955640812095}, + {7.483148574829102, 46.945311085627445}, + {7.487225532531738, 46.957029058564686}, + {7.4793291091918945, 46.96388288331302}, + {7.464480400085448, 46.96903731827891}, + {7.452635765075683, 46.96692874582506}, + }}, + {{ + {7.4478721618652335, 47.00015837528636}, + {7.5110435485839835, 47.00015837528636}, + {7.5110435485839835, 47.00683108710118}, + {7.4478721618652335, 47.00683108710118}, + {7.4478721618652335, 47.00015837528636}, + }}, + } + + doc = document.NewDocument("multipolygon4") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipolygon4, "multipolygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} diff --git a/search/searcher/search_geoshape_linestring_test.go b/search/searcher/search_geoshape_linestring_test.go new file mode 100644 index 0000000..dc82df3 --- /dev/null +++ b/search/searcher/search_geoshape_linestring_test.go @@ -0,0 +1,687 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoJsonLinestringIntersectsQuery(t *testing.T) { + tests := []struct { + line [][]float64 + field string + want []string + }{ + // test intersecting linestring query for polygon1. + { + [][]float64{ + {74.85860824584961, 22.407219759334023}, + {74.8663330078125, 22.382936446589863}, + }, + "geometry", + []string{"polygon1"}, + }, + + // test intersecting linestring query for polygon1 and polygon2. + { + [][]float64{ + {74.82461929321289, 22.393729553598526}, + {74.93671417236328, 22.356743809494784}, + }, + "geometry", + []string{"polygon1", "polygon2"}, + }, + + // test intersecting linestring query for envelope1. + { + [][]float64{ + {74.83938217163086, 22.325782524687973}, + {74.8692512512207, 22.311172762889516}, + }, + "geometry", + []string{"envelope1"}, + }, + + // test intersecting linestring query for circle. + { + [][]float64{ + {74.94546890258789, 22.310815439776572}, + {74.93276596069336, 22.303708490145645}, + }, + "geometry", + []string{"circle1"}, + }, + + // test intersecting linestring query for linestring1. + { + [][]float64{ + {74.938645362854, 22.321614134448936}, + {74.94070529937744, 22.320224643365446}, + }, + "geometry", + []string{"linestring1"}, + }, + + // test intersecting linestring query for multilinestring1. + { + [][]float64{ + {74.9241828918457, 22.307996525380194}, + {74.94100570678711, 22.293781977618558}, + }, + "geometry", + []string{"multilinestring1"}, + }, + + // test intersecting linestring query for multipolygon1. + { + [][]float64{ + {36.22072219848633, 50.007132228568786}, + {36.22218132019043, 49.99791917183082}, + }, + "geometry", + []string{"multipolygon1"}, + }, + + // test intersecting linestring query for envelope2, circle2, + // multipolygon1 and gc_polygonInGc_multipolygonInGc. + { + [][]float64{ + {36.19840621948242, 50.03834418692451}, + {36.25720024108887, 50.02136210283289}, + }, + "geometry", + []string{"envelope2", "circle2", "multipolygon1", "gc_polygonInGc_multipolygonInGc"}, + }, + } + i := setupGeoJsonShapesIndexForLinestringQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeLinestringQueryWithRelation("intersects", + indexReader, test.line, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.line) + } + } +} + +func TestGeoJsonLinestringContainsQuery(t *testing.T) { + tests := []struct { + line [][]float64 + field string + want []string + }{ + // test a linestring query for multipolygon1. + { + [][]float64{ + {36.21668815612793, 50.040494087443996}, + {36.226301193237305, 50.03861982057644}, + }, + "geometry", + []string{"multipolygon1"}, + }, + + // test a linestring query with endspoints on two + // different polygons in a multipolygon. + { + [][]float64{ + {36.19746208190918, 50.038564693972646}, + {36.21565818786621, 50.03718650830641}, + }, + "geometry", nil, + }, + + // test a linestring query for envelope2. + { + [][]float64{ + {36.25290870666503, 50.03018471417061}, + {36.23110771179199, 50.01854955486945}, + }, + "geometry", + []string{"envelope2"}, + }, + + // test a linestring query for circle2. + { + [][]float64{ + {36.220550537109375, 50.02930252595981}, + {36.224327087402344, 50.02847545979485}, + }, + "geometry", + []string{"circle2"}, + }, + + // test a linestring query for polygonWithHole2. + { + [][]float64{ + {36.27367973327637, 49.89883638369706}, + {36.27445220947265, 49.89596137883285}, + }, + "geometry", + []string{"polygonWithHole2"}, + }, + + // test a linestring query within the hole of polygonWithHole2. + {[][]float64{ + {36.261234283447266, 49.89540847364305}, + {36.26243591308594, 49.89087441212101}, + }, "geometry", nil}, + } + i := setupGeoJsonShapesIndexForLinestringQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeLinestringQueryWithRelation("contains", + indexReader, test.line, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.line) + } + } +} + +func TestGeoJsonMultiLinestringContainsQuery(t *testing.T) { + tests := []struct { + line [][][]float64 + field string + want []string + }{ + // test a multilinestring query for multipolygon1. + { + [][][]float64{ + { + {36.21668815612793, 50.040494087443996}, + {36.226301193237305, 50.03861982057644}, + }, + { + {36.226816177368164, 49.999463999158}, + {36.234025955200195, 50.00271900853649}, + }, + }, + "geometry", + []string{"multipolygon1"}, + }, + + // test a multilinestring query that is covered by the geometryCollection. + { + [][][]float64{{ + {36.28664016723633, 49.96574238290487}, + {36.30251884460449, 49.96369956194569}, + }, { + {36.19179725646973, 50.03983258984584}, + {36.19420051574707, 50.03801342445342}, + }}, + "geometry", + []string{"gc_polygonInGc_multipolygonInGc"}, + }, + + // test a multilinestring query for envelope2. + { + [][][]float64{ + { + {36.23213768005371, 50.02913711386621}, + {36.25187873840332, 50.02902683882067}, + }, + { + {36.231794357299805, 50.018935600613254}, + {36.2314510345459, 50.025883893582055}, + }, + }, + "geometry", + []string{"envelope2"}, + }, + + // test a multilinestring query with one linestring outside of envelope2. + { + [][][]float64{ + { + {36.23213768005371, 50.02913711386621}, + {36.25187873840332, 50.02902683882067}, + }, + {{36.231794357299805, 50.018935600613254}, {36.2314510345459, 50.025883893582055}}, + {{36.25659942626953, 50.024284772330844}, {36.24406814575195, 50.01518531066489}}, + }, + "geometry", nil, + }, + + // test a multilinestring query with one linestring + // inside the whole of a polygonWithHole2. + { + [][][]float64{ + { + {36.27367973327637, 49.89883638369706}, + {36.27445220947265, 49.89596137883285}, + }, + {{36.261234283447266, 49.89540847364305}, {36.26243591308594, 49.89087441212101}}, + }, + "geometry", nil, + }, + + // test a multilinestring query for polygonWithHole2. + { + [][][]float64{ + { + {36.27367973327637, 49.89883638369706}, + {36.27445220947265, 49.89596137883285}, + }, + {{36.279258728027344, 49.894302644257856}, {36.28166198730469, 49.887335336408235}}, + }, + "geometry", + []string{"polygonWithHole2"}, + }, + + // test a multilinestring query for polygonWithHole2 with last line cross the hole. + { + [][][]float64{ + { + {36.27367973327637, 49.89883638369706}, + {36.27445220947265, 49.89596137883285}, + }, + {{36.279258728027344, 49.894302644257856}, {36.28166198730469, 49.887335336408235}}, + {{36.254024505615234, 49.89839408640621}, {36.27016067504883, 49.90038439228633}}, + }, + "geometry", nil, + }, + } + i := setupGeoJsonShapesIndexForLinestringQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeMultiLinestringQueryWithRelation("contains", + indexReader, test.line, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.line) + } + } +} + +func runGeoShapeMultiLinestringQueryWithRelation(relation string, i index.IndexReader, + points [][][]float64, field string, +) ([]string, error) { + s := geo.NewGeoJsonMultilinestring(points) + return executeSearch(relation, i, s, field) +} + +func runGeoShapeLinestringQueryWithRelation(relation string, i index.IndexReader, + points [][]float64, field string, +) ([]string, error) { + s := geo.NewGeoJsonLinestring(points) + return executeSearch(relation, i, s, field) +} + +func executeSearch(relation string, i index.IndexReader, + s index.GeoJSON, field string, +) ([]string, error) { + var rv []string + gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + docID, _ := i.ExternalID(docMatch.IndexInternalID) + rv = append(rv, docID) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeoJsonShapesIndexForLinestringQuery(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + polygon1 := [][][][]float64{{{ + {74.84642028808594, 22.402776071459712}, + {74.83234405517578, 22.39039647758608}, + {74.86719131469727, 22.38801566009795}, + {74.85139846801758, 22.39103135536648}, + {74.86461639404297, 22.394840561182853}, + {74.8495101928711, 22.397697397065034}, + {74.86186981201172, 22.401982540816856}, + {74.84642028808594, 22.402776071459712}, + }}} + doc := document.NewDocument("polygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, polygon1, "polygon", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon2 := [][][][]float64{{{ + {74.93431091308592, 22.376428433285266}, + {74.92898941040039, 22.39103135536648}, + {74.9241828918457, 22.37722210974017}, + {74.90821838378906, 22.37388863821397}, + {74.92504119873047, 22.369920115637292}, + {74.92864608764648, 22.355632497760894}, + {74.93207931518555, 22.370396344320053}, + {74.94855880737305, 22.3743648533201}, + {74.93431091308592, 22.376428433285266}, + }}} + doc = document.NewDocument("polygon2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, polygon2, "polygon", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + envelope1 := [][][][]float64{{{ + {74.86736297607422, 22.307361269208684}, + {74.87028121948242, 22.345471522338478}, + }}} + doc = document.NewDocument("envelope1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, envelope1, "envelope", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + envelope2 := [][][][]float64{{{ + {36.23007774353027, 50.01810835593541}, + {36.25333786010742, 50.03068093791795}, + }}} + doc = document.NewDocument("envelope2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, envelope2, "envelope", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle1") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", + []uint64{}, []float64{74.93671417236328, 22.308314152382284}, "300m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle2") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", + []uint64{}, []float64{36.22243881225586, 50.02941280037234}, "600m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring := [][][][]float64{{{ + {74.92697238922119, 22.320343743143248}, + {74.94036197662354, 22.32054224254707}, + }}} + doc = document.NewDocument("linestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, linestring, "linestring", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring1 := [][][][]float64{{{ + {77.60188579559325, 12.982604078764705}, + {77.60557651519775, 12.987329508048184}, + }}} + doc = document.NewDocument("linestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, linestring1, "linestring", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring := [][][][]float64{{ + { + {74.92203712463379, 22.3113315728684}, + {74.92323875427246, 22.307798008137024}, + }, + {{74.92405414581299, 22.307559787072712}, {74.92735862731934, 22.310021385140573}}, + {{74.9223804473877, 22.311688894660474}, {74.92534160614014, 22.30930673210729}}, + }} + doc = document.NewDocument("multilinestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, multilinestring, "multilinestring", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring1 := [][][][]float64{{ + { + {77.6015853881836, 12.990089451715061}, + {77.60476112365723, 12.987747683302153}, + }, + {{77.59875297546387, 12.988751301039581}, {77.59446144104004, 12.98197680263484}}, + {{77.60188579559325, 12.982604078764705}, {77.60557651519775, 12.987329508048184}}, + }} + doc = document.NewDocument("multilinestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, multilinestring1, "multilinestring", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipoint1 := [][][][]float64{{{ + {77.56618022918701, 12.958180959662695}, + {77.56407737731932, 12.951614746607163}, + {77.56922721862793, 12.956173473406446}, + }}} + doc = document.NewDocument("multipoint1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, multipoint1, "multipoint", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygonWithHole1 := [][][][]float64{{ + { + {77.59991168975829, 12.972232910164502}, + {77.6039457321167, 12.97582941279006}, + {77.60424613952637, 12.98168407323241}, + {77.59974002838135, 12.985489528568463}, + {77.59321689605713, 12.979300406693417}, + {77.59991168975829, 12.972232910164502}, + }, + { + {77.59682178497314, 12.975787593290978}, + {77.60295867919922, 12.975787593290978}, + {77.60295867919922, 12.98143316204164}, + {77.59682178497314, 12.98143316204164}, + {77.59682178497314, 12.975787593290978}, + }, + }} + + doc = document.NewDocument("polygonWithHole1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, polygonWithHole1, "polygon", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygonWithHole2 := [][][][]float64{{ + { + {36.261234283447266, 49.90712870720605}, + {36.2479305267334, 49.89480027061714}, + {36.254539489746094, 49.883408870659736}, + {36.280717849731445, 49.883408870659736}, + {36.28741264343262, 49.890432041848264}, + {36.27788543701172, 49.90276159448742}, + {36.261234283447266, 49.90712870720605}, + }, + + { + {36.264581680297844, 49.905249238801304}, + {36.25368118286133, 49.89673543545543}, + {36.253509521484375, 49.88578690918283}, + {36.270332336425774, 49.886174020645804}, + {36.27127647399902, 49.89579550794111}, + {36.264581680297844, 49.905249238801304}, + }, + }} + + doc = document.NewDocument("polygonWithHole2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, polygonWithHole2, "polygon", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipolygon1 := [][][][]float64{{{ + {36.1875057220459, 50.04363607656457}, + {36.192398071289055, 50.034871067327856}, + {36.20218276977539, 50.03955696315653}, + {36.1875057220459, 50.04363607656457}, + }}, // polygon1 + {{ + {36.2123966217041, 50.03795829715335}, + {36.218318939208984, 50.0333273779768}, + {36.226558685302734, 50.03867494711694}, + {36.217031478881836, 50.04286437899031}, + {36.2123966217041, 50.03795829715335}, + }}, // polygon2 + {{ + {36.221065521240234, 50.00365685169585}, + {36.226301193237305, 49.998029518286025}, + {36.23342514038086, 49.9995743420677}, + {36.23531341552734, 50.002994846659156}, + {36.231021881103516, 50.00630478067617}, + {36.22810363769531, 50.00663576154257}, + {36.226043701171875, 50.004815338573046}, + {36.221065521240234, 50.00365685169585}, + }}} + doc = document.NewDocument("multipolygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", + []uint64{}, multipolygon1, "multipolygon", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygonInGc := [][][][]float64{{{ + {36.1875057220459, 50.04363607656457}, + {36.192398071289055, 50.034871067327856}, + {36.20218276977539, 50.03955696315653}, + {36.1875057220459, 50.04363607656457}, + }}} + multipolygonInGc := [][][][]float64{{{ + {36.29015922546387, 49.980150089789376}, + {36.28337860107422, 49.961656654293485}, + {36.307411193847656, 49.96033147865059}, + {36.29015922546387, 49.980150089789376}, + }}, // polygon1 + {{ + {36.16106986999512, 50.00387751801547}, + {36.161842346191406, 49.9908012905034}, + {36.17900848388672, 49.99841572888488}, + {36.16106986999512, 50.00387751801547}, + }}} + coordinates := [][][][][]float64{polygonInGc, multipolygonInGc} + types := []string{"polygon", "multipolygon"} + doc = document.NewDocument("gc_polygonInGc_multipolygonInGc") + doc.AddField(document.NewGeometryCollectionFieldWithIndexingOptions("geometry", + []uint64{}, coordinates, types, + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} diff --git a/search/searcher/search_geoshape_points_test.go b/search/searcher/search_geoshape_points_test.go new file mode 100644 index 0000000..43ca74e --- /dev/null +++ b/search/searcher/search_geoshape_points_test.go @@ -0,0 +1,600 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoJsonPointContainsQuery(t *testing.T) { + tests := []struct { + point []float64 + field string + want []string + }{ + // test points inside the polygon1. + { + []float64{77.58334636688232, 12.948268838994263}, + "geometry", + []string{"polygon1"}, + }, + + // test points inside the circle1. + { + []float64{77.58553504943848, 12.954040501528555}, + "geometry", + []string{"circle1"}, + }, + + // test points inside the polygon1 and the circle. + { + []float64{77.59293794631958, 12.948896200093982}, + "geometry", + []string{"polygon1", "circle1"}, + }, + + // test points outside the polygon1 and the circle1. + { + []float64{77.5614595413208, 12.953287683563568}, + "geometry", nil, + }, + + // test point within the envelope1. + { + []float64{81.28166198730469, 26.34203746601541}, + "geometry", + []string{"envelope1"}, + }, + + // test point on the linestring vertex. + { + []float64{77.57776737213135, 12.952074805390097}, + "geometry", + []string{"linestring1"}, + }, + + // test point on the multilinestring vertex. + { + []float64{77.5779390335083, 12.945006535817749}, + "geometry", + []string{"multilinestring1"}, + }, + + // test point on the multipoint vertex. + { + []float64{77.56407737731932, 12.951614746607163}, + "geometry", + []string{"multipoint1"}, + }, + + // test point within the polygonWithHole1. + { + []float64{77.60334491729736, 12.979844051951334}, + "geometry", + []string{"polygonWithHole1"}, + }, + + // test point within the hole of the polygonWithHole1. + { + []float64{77.60244369506836, 12.976247607394027}, + "geometry", nil, + }, + } + i := setupGeoJsonShapesIndex(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePointRelationQuery("contains", + false, indexReader, [][]float64{test.point}, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.point) + } + } +} + +func TestGeoJsonMultiPointWithInQuery(t *testing.T) { + tests := []struct { + multipoint [][]float64 + field string + want []string + }{ + // test multipoint inside the polygon1. + { + [][]float64{ + {77.58334636688232, 12.948268838994263}, + {77.58467674255371, 12.944295515355652}, + }, + "geometry", + []string{"polygon1"}, + }, + + // test multipoint inside the circle1. + { + [][]float64{ + {77.58553504943848, 12.954040501528555}, + {77.58643627166747, 12.956089827794571}, + }, + "geometry", + []string{"circle1"}, + }, + + // test multipoint inside the envelope1. + { + [][]float64{ + {81.28166198730469, 26.34203746601541}, + {80.94314575195312, 26.346960121309415}, + }, + "geometry", + []string{"envelope1"}, + }, + + // test multipoint inside the polygon1 and the circle. + { + [][]float64{ + {77.59293794631958, 12.948896200093982}, + {77.58532047271729, 12.953789562459688}, + }, + "geometry", + []string{"polygon1", "circle1"}, + }, + + // test multipoint (only 1 point outside) outside. + {[][]float64{ + {77.58334636688232, 12.948268838994263}, + {77.58643627166747, 12.956089827794571}, + {77.5615, 12.9533}, + }, "geometry", nil}, + + // test multipoint on the linestring vertex. + { + [][]float64{ + {77.5841188430786, 12.957093573282744}, + {77.57776737213135, 12.952074805390097}, + }, + "geometry", + []string{"linestring1"}, + }, + + // test multipoint outside the linestring vertex. + { + [][]float64{ + {77.5841188430786, 12.957093573282744}, + {77.57776737213135, 12.952074805390097}, + {77.58334636688232, 12.948268838994263}, + }, + "geometry", nil, + }, + + // test multipoint on the multilinestring vertex. + { + [][]float64{ + {77.5779390335083, 12.94471376293191}, + {77.57218837738037, 12.948268838994263}, + }, + "geometry", + []string{"multilinestring1"}, + }, + + // test multipoint outside the multilinestring vertex. + { + [][]float64{ + {77.5779390335083, 12.94471376293191}, + {77.57218837738037, 12.948268838994263}, + {77.58532047271729, 12.953789562459688}, + }, + "geometry", nil, + }, + + // test multipoint with one inside the hole within the polygonWithHole1. + { + [][]float64{ + {77.60334491729736, 12.979844051951334}, + {77.60244369506836, 12.976247607394027}, + }, + "geometry", nil, + }, + + // test multipoint with all inside the hole witin the polygonWithHole1. + { + [][]float64{ + {77.59656429290771, 12.981767710239714}, + {77.59888172149658, 12.979969508380469}, + }, + "geometry", nil, + }, + + // test multipoint with all inside the polygonWithHole1. + { + [][]float64{ + {77.60334491729736, 12.979844051951334}, + {77.59656429290771, 12.981767710239714}, + {77.59802341461182, 12.9751602999608}, + }, + "geometry", + []string{"polygonWithHole1"}, + }, + } + i := setupGeoJsonShapesIndex(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePointRelationQuery("contains", + true, indexReader, test.multipoint, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.multipoint) + } + } +} + +func TestGeoJsonMultiPointIntersectsQuery(t *testing.T) { + tests := []struct { + multipoint [][]float64 + field string + want []string + }{ + // test multipoint inside the polygon1. + { + [][]float64{ + {77.58334636688232, 12.948268838994263}, + {77.58467674255371, 12.944295515355652}, + }, + "geometry", + []string{"polygon1"}, + }, + + // test multipoint inside the circle1. + { + [][]float64{ + {77.58553504943848, 12.954040501528555}, + {77.58643627166747, 12.956089827794571}, + }, + "geometry", + []string{"circle1"}, + }, + + // test multipoint inside the envelope1. (1 point outside) + { + [][]float64{ + {81.28166198730469, 26.34203746601541}, + {80.94314575195312, 26.346960121309415}, + {81.12716674804688, 26.353728430338332}, + }, + "geometry", + []string{"envelope1"}, + }, + + // test multipoint inside the polygon1 and the circle. + { + [][]float64{ + {77.59293794631958, 12.948896200093982}, + {77.58532047271729, 12.953789562459688}, + }, + "geometry", + []string{"polygon1", "circle1"}, + }, + + // test multipoint (only 1 point outside) intersects. + { + [][]float64{ + {77.58334636688232, 12.948268838994263}, + {77.58643627166747, 12.956089827794571}, + {77.5615, 12.9533}, + }, + "geometry", + []string{"polygon1", "circle1"}, + }, + + // test multipoint on the linestring vertex. + { + [][]float64{ + {77.5841188430786, 12.957093573282744}, + {77.57776737213135, 12.952074805390097}, + }, + "geometry", + []string{"linestring1"}, + }, + + // test multipoint outside the linestring vertex. + { + [][]float64{ + {77.5841188430786, 12.957093573282744}, + {77.57776737213135, 12.952074805390097}, + {77.58334636688232, 12.948268838994263}, + }, + "geometry", + []string{"polygon1", "linestring1"}, + }, + + // test multipoint on the multilinestring vertex. + { + [][]float64{ + {77.5779390335083, 12.94471376293191}, + {77.57218837738037, 12.948268838994263}, + }, + "geometry", + []string{"multilinestring1"}, + }, + + // test multipoint outside the multilinestring vertex. + { + [][]float64{ + {77.5779390335083, 12.94471376293191}, + {77.57218837738037, 12.948268838994263}, + {77.58532047271729, 12.953789562459688}, + }, + "geometry", + []string{"polygon1", "circle1", "multilinestring1"}, + }, + + // test multipoint with one inside the hole within the polygonWithHole1. + { + [][]float64{ + {77.60334491729736, 12.979844051951334}, + {77.60244369506836, 12.976247607394027}, + }, + "geometry", + []string{"polygonWithHole1"}, + }, + + // test multipoint with all inside the hole witin the polygonWithHole1. + { + [][]float64{ + {77.60244369506836, 12.976247607394027}, + {77.59888172149658, 12.979969508380469}, + }, + "geometry", nil, + }, + + // test multipoint with all inside the polygonWithHole1. + { + [][]float64{ + {77.60334491729736, 12.979844051951334}, + {77.59656429290771, 12.981767710239714}, + {77.59802341461182, 12.9751602999608}, + }, + "geometry", + []string{"polygonWithHole1"}, + }, + } + i := setupGeoJsonShapesIndex(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePointRelationQuery("intersects", + true, indexReader, test.multipoint, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.multipoint) + } + } +} + +func runGeoShapePointRelationQuery(relation string, multi bool, + i index.IndexReader, points [][]float64, field string, +) ([]string, error) { + var rv []string + var s index.GeoJSON + if multi { + s = geo.NewGeoJsonMultiPoint(points) + } else { + s = geo.NewGeoJsonPoint(points[0]) + } + + gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + docID, _ := i.ExternalID(docMatch.IndexInternalID) + rv = append(rv, docID) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +type Fatalfable interface { + Fatalf(format string, args ...interface{}) +} + +func setupGeoJsonShapesIndex(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + polygon1 := [][][][]float64{{{ + {77.5853419303894, 12.953977766785052}, + {77.58405447006226, 12.95393594361393}, + {77.5819730758667, 12.9495026476557}, + {77.58068561553955, 12.94883346405509}, + {77.58019208908081, 12.948331575175299}, + {77.57991313934326, 12.943814529775414}, + {77.58497714996338, 12.94394000436408}, + {77.58517026901245, 12.9446301134728}, + {77.58572816848755, 12.945508431393435}, + {77.58785247802734, 12.946365833997325}, + {77.58967638015747, 12.946428570657417}, + {77.59070634841918, 12.947474179333993}, + {77.59317398071289, 12.948875288082773}, + {77.59167194366454, 12.949962710338657}, + {77.59077072143555, 12.950276388953625}, + {77.59098529815674, 12.951196510612728}, + {77.58729457855225, 12.952472128200755}, + {77.5853419303894, 12.953977766785052}, + }}} + doc := document.NewDocument("polygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + // not working envelope + envelope1 := [][][][]float64{{{ + {80.93696594238281, 26.33957605983274}, + {81.28440856933594, 26.351267272877074}, + }}} + doc = document.NewDocument("envelope1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + envelope1, "envelope", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle1") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", []uint64{}, + []float64{77.59137153625487, 12.952660333521468}, "900m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring := [][][][]float64{{{ + {77.5841188430786, 12.957093573282744}, + {77.57776737213135, 12.952074805390097}, + }}} + doc = document.NewDocument("linestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring := [][][][]float64{{{ + {77.57227420806883, 12.948687079902895}, + {77.57600784301758, 12.954165970968194}, + {77.5779390335083, 12.94471376293191}, + {77.57218837738037, 12.948268838994263}, + {77.57781028747559, 12.951740217268595}, + {77.5779390335083, 12.945006535817749}, + }}} + doc = document.NewDocument("multilinestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipoint1 := [][][][]float64{{{ + {77.56618022918701, 12.958180959662695}, + {77.56407737731932, 12.951614746607163}, + {77.56922721862793, 12.956173473406446}, + }}} + doc = document.NewDocument("multipoint1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipoint1, "multipoint", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygonWithHole1 := [][][][]float64{{ + { + {77.59991168975829, 12.972232910164502}, + {77.6039457321167, 12.97582941279006}, + {77.60424613952637, 12.98168407323241}, + {77.59974002838135, 12.985489528568463}, + {77.59321689605713, 12.979300406693417}, + {77.59991168975829, 12.972232910164502}, + }, + { + {77.59682178497314, 12.975787593290978}, + {77.60295867919922, 12.975787593290978}, + {77.60295867919922, 12.98143316204164}, + {77.59682178497314, 12.98143316204164}, + {77.59682178497314, 12.975787593290978}, + }, + }} + + doc = document.NewDocument("polygonWithHole1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygonWithHole1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} diff --git a/search/searcher/search_geoshape_polygon_test.go b/search/searcher/search_geoshape_polygon_test.go new file mode 100644 index 0000000..98be1c4 --- /dev/null +++ b/search/searcher/search_geoshape_polygon_test.go @@ -0,0 +1,1213 @@ +// Copyright (c) 2022 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestGeoJsonPolygonIntersectsQuery(t *testing.T) { + tests := []struct { + polygon [][][]float64 + field string + want []string + }{ + // test intersecting query polygon for polygon1. + {[][][]float64{{ + {77.57926940917969, 12.945257483731918}, + {77.57875442504883, 12.942036966318216}, + {77.58278846740721, 12.9424970427816}, + {77.57926940917969, 12.945257483731918}, + }}, "geometry", []string{"polygon1"}}, + + // test intersecting query polygon for polygon1, polygon2, circle1. + { + [][][]float64{{ + {77.59562015533446, 12.94099133483504}, + {77.59665012359619, 12.949356263896634}, + {77.59313106536865, 12.951321981484776}, + {77.59085655212402, 12.948477959536318}, + {77.59562015533446, 12.94099133483504}, + }}, + "geometry", + []string{"polygon1", "polygon2", "circle1"}, + }, + + // test intersecting query polygon for polygon1, polygon2 and polygon3. + {[][][]float64{{ + {77.5929594039917, 12.939151012774925}, + {77.58321762084961, 12.94546660680072}, + {77.59737968444824, 12.931998723107322}, + {77.60111331939697, 12.955169724209911}, + {77.59592056274414, 12.936265025833965}, + {77.5929594039917, 12.939151012774925}, + }}, "geometry", []string{"polygon1", "polygon2", "polygon3"}}, + + // test intersecting query polygon for polygon2 and the circle1. + { + [][][]float64{{ + {77.59012699127197, 12.959853852513307}, + {77.59836673736572, 12.959853852513307}, + {77.59836673736572, 12.965541604118611}, + {77.59012699127197, 12.965541604118611}, + {77.59012699127197, 12.959853852513307}, + }}, + "geometry", + []string{"polygon2", "circle1"}, + }, + + // test intersecting query polygon for linestring2 and multilinestring2. + { + [][][]float64{{ + {77.59669303894043, 12.989504011681609}, + {77.60699272155762, 12.983231353311314}, + {77.60115623474121, 12.993183897537897}, + {77.59669303894043, 12.989504011681609}, + }}, + "geometry", + []string{"linestring2", "multilinestring2"}, + }, + + // test intersecting query polygon for multilinestring2. + { + [][][]float64{{ + {77.60124206542969, 12.987162237749484}, + {77.60330200195312, 12.992849364713313}, + {77.59514808654785, 12.989671280403403}, + {77.60124206542969, 12.987162237749484}, + }}, + "geometry", + []string{"multilinestring2"}, + }, + + // test intersecting query polygon for multipoint1. + { + [][][]float64{{ + {77.56648063659668, 12.956382587313202}, + {77.56819725036621, 12.949523559614263}, + {77.5718879699707, 12.958222782120954}, + {77.56648063659668, 12.956382587313202}, + }}, + "geometry", + []string{"multipoint1"}, + }, + + // test intersecting query polygon for envelope1. + {[][][]float64{{ + {36.19986534118652, 50.00034673534484}, + {36.19351387023926, 50.00464984215712}, + {36.178321838378906, 49.991573824716205}, + {36.19986534118652, 50.00034673534484}, + }}, "geometry", []string{"envelope1"}}, + + // test intersecting query polygon for envelope1. + {[][][]float64{{ + {36.170082092285156, 49.99229116680205}, + {36.14982604980469, 49.99002874388075}, + {36.227073669433594, 49.98754547425633}, + {36.170082092285156, 49.99229116680205}, + }}, "geometry", []string{"envelope1"}}, + } + i := setupGeoJsonShapesIndexForPolygonQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePolygonQueryWithRelation("intersects", + indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", n, + test.want, got, test.polygon) + } + } +} + +func TestGeoJsonPolygonContainsQuery(t *testing.T) { + tests := []struct { + polygon [][][]float64 + field string + want []string + }{ + // test containment query polygon for polygon1. + { + [][][]float64{{ + {77.5843334197998, 12.952702156906767}, + {77.58510589599608, 12.952702156906767}, + {77.58510589599608, 12.953622269606669}, + {77.5843334197998, 12.953622269606669}, + {77.5843334197998, 12.952702156906767}, + }}, + "geometry", + []string{"polygon1"}, + }, + + // test containment query polygon for circle1. + { + [][][]float64{{ + {77.59025573730469, 12.953810474058429}, + {77.59145736694336, 12.953810474058429}, + {77.59145736694336, 12.954918786278716}, + {77.59025573730469, 12.954918786278716}, + {77.59025573730469, 12.953810474058429}, + }}, + "geometry", + []string{"circle1"}, + }, + + // test containment query polygon for polygon2, polygon3. + { + [][][]float64{{ + {77.60235786437988, 12.956884459972992}, + {77.60124206542969, 12.956800814599926}, + {77.6008129119873, 12.955713422193524}, + {77.60244369506836, 12.955211547173878}, + {77.60313034057617, 12.955880713641998}, + {77.60235786437988, 12.956884459972992}, + }}, + "geometry", + []string{"polygon2", "polygon3"}, + }, + + // test containment query polygon which resides within a hole in polygonWithHole1. + { + [][][]float64{{ + {77.60012626647949, 12.97963495776207}, + {77.5978946685791, 12.978213112610835}, + {77.60089874267577, 12.977962197916442}, + {77.60012626647949, 12.97963495776207}, + }}, + "geometry", nil, + }, + + // test containment query polygon which resides within polygonWithHole1. + { + [][][]float64{{ + {77.59978294372559, 12.984067716910454}, + {77.59780883789062, 12.982227713276774}, + {77.60089874267577, 12.982227713276774}, + {77.59978294372559, 12.984067716910454}, + }}, + "geometry", + []string{"polygonWithHole1"}, + }, + + // test with query polygon for polygon4 with a single vertex lying outside. + { + [][][]float64{{ + {-121.48138761520384, 38.50964107572585}, + {-121.48226737976073, 38.509238097766875}, + {-121.48115158081055, 38.50781086602439}, + {-121.48014307022095, 38.50806273250507}, + {-121.48138761520384, 38.50964107572585}, + }}, + "geometry", nil, + }, + + // test with query polygon for polygon4. + { + [][][]float64{{ + {-121.48381233215332, 38.507974579337045}, + {-121.48361384868622, 38.507869634948676}, + {-121.48361921310425, 38.50765135013098}, + {-121.48343682289122, 38.50797038156446}, + {-121.48381233215332, 38.507974579337045}, + }}, + "geometry", + []string{"polygon4"}, + }, + + // test with query polygon for multipolygon1. + {[][][]float64{{ + {-121.47578716278075, 38.51617236229197}, + {-121.47578716278075, 38.51566868518406}, + {-121.47546529769896, 38.516105205547866}, + {-121.47578716278075, 38.51617236229197}, + }}, "geometry", []string{"multipolygon1"}}, + + // test with query polygon for envelope1. + { + [][][]float64{{ + {36.197547912597656, 49.99642946989866}, + {36.18939399719238, 49.988649165474}, + {36.20201110839844, 49.98853879749191}, + {36.1970329284668, 49.980150089789376}, + { + 36.205787658691406, + 49.9885939815146, + }, + {36.197547912597656, 49.99642946989866}, + }}, + "geometry", + []string{"envelope1"}, + }, + + // test with query polygon for no hits. (envelope1 has one vertex outside the polygon) + {[][][]float64{{ + {36.19832038879394, 49.99626394461266}, + {36.19016647338867, 49.98439981533724}, + {36.20698928833008, 49.98158510403259}, + {36.19832038879394, 49.99626394461266}, + }}, "geometry", nil}, + } + i := setupGeoJsonShapesIndexForPolygonQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePolygonQueryWithRelation("contains", + indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.polygon) + } + } +} + +func TestGeoJsonPolygonWithInQuery(t *testing.T) { + tests := []struct { + polygon [][][]float64 + field string + want []string + }{ + // test with query polygon for polygon1. + { + [][][]float64{{ + {77.58407592773438, 12.956382587313202}, + {77.57746696472168, 12.943249893344905}, + {77.5920581817627, 12.944086391304364}, + {77.59454727172852, 12.95353862313803}, + {77.58407592773438, 12.956382587313202}, + }}, + "geometry", + []string{"polygon1"}, + }, + + // test with query polygon for circle1 and polygon3. + { + [][][]float64{{ + {77.59248733520508, 12.967841760870071}, + {77.58261680603027, 12.968594534825176}, + {77.57789611816406, 12.957302686416881}, + {77.58896827697754, 12.945341132980488}, + {77.60450363159178, 12.947599652080394}, + {77.60673522949219, 12.96483064227584}, + {77.59248733520508, 12.967841760870071}, + }}, + "geometry", + []string{"polygon3", "circle1"}, + }, + + // test with query polygon for linestring2, multilinestring2. + { + [][][]float64{{ + {77.59909629821777, 12.998118204343788}, + {77.58931159973145, 12.978882217224443}, + {77.61128425598145, 12.983565899088745}, + {77.59909629821777, 12.998118204343788}, + }}, + "geometry", + []string{"linestring2", "multilinestring2"}, + }, + + // test with query polygon for multipoint1. + {[][][]float64{{ + {77.55703926086426, 12.964245142762644}, + {77.5631332397461, 12.944253690559432}, + {77.57429122924805, 12.957720912158363}, + {77.55703926086426, 12.964245142762644}, + }}, "geometry", []string{"multipoint1"}}, + + // test with query polygon with no results. + // (polygon4 has one vertex lying outside the query polygon). + { + [][][]float64{{ + {-121.48812532424927, 38.51058134885975}, + {-121.48258924484252, 38.500153704565065}, + {-121.47492885589598, 38.50799556819636}, + {-121.48630142211913, 38.51147123890908}, + {-121.48812532424927, 38.51058134885975}, + }}, + "geometry", nil, + }, + + // test with query polygon for polygon4. + {[][][]float64{{ + {-121.48366212844849, 38.510161585585045}, + {-121.48533582687377, 38.50841534409804}, + {-121.48376941680908, 38.507777283760426}, + {-121.48370504379272, 38.50250467407243}, + {-121.48010015487672, 38.50253825879518}, + {-121.48018598556519, 38.504502937819765}, + {-121.47756814956665, 38.50755899866278}, + {-121.48113012313843, 38.50866720846446}, + {-121.48115158081055, 38.51017837616302}, + {-121.48366212844849, 38.510161585585045}, + }}, "geometry", []string{"polygon4"}}, + + // test with query polygon for envelope1. + { + [][][]float64{{ + {36.20587348937988, 50.00470500769241}, + {36.17969512939453, 49.993946530777606}, + {36.19368553161621, 49.971870325635074}, + {36.21119499206543, 49.983075265826656}, + {36.20587348937988, 50.00470500769241}, + }}, + "geometry", + []string{"envelope1"}, + }, + + // test with query polygon for linestring2 which lies outside except the endpoints. + { + [][][]float64{{ + {8.515305519104004, 47.392597129887}, + {8.514232635498047, 47.38896544894171}, + {8.507537841796875, 47.38815191810328}, + {8.514318466186523, 47.38725120859953}, + {8.516035079956053, 47.383357642070706}, + {8.516979217529295, 47.38733837470806}, + {8.522472381591797, 47.38794853343167}, + {8.516507148742676, 47.388994503382285}, + {8.515305519104004, 47.392597129887}, + }}, + "geometry", nil, + }, + + // test with query polygon for all the shapes. + { + [][][]float64{{ + {-135.0, -38.0}, + {149.0, -38.0}, + {149.0, 77.0}, + {-135.0, 77.0}, + }}, + "geometry", + []string{ + "polygon1", "polygon2", "polygon3", "envelope1", "circle1", "linestring1", + "linestring2", "linestring3", "multilinestring1", "multilinestring2", "multipoint1", + "polygonWithHole1", "polygon4", "multipolygon1", + }, + }, + } + + i := setupGeoJsonShapesIndexForPolygonQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePolygonQueryWithRelation("within", + indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.polygon) + } + } +} + +func runGeoShapePolygonQueryWithRelation(relation string, i index.IndexReader, + points [][][]float64, field string, +) ([]string, error) { + var rv []string + s := geo.NewGeoJsonPolygon(points) + + gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + docID, _ := i.ExternalID(docMatch.IndexInternalID) + rv = append(rv, docID) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeoJsonShapesIndexForPolygonQuery(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + polygon1 := [][][][]float64{{{ + {77.5853419303894, 12.953977766785052}, + {77.58405447006226, 12.95393594361393}, + {77.5819730758667, 12.9495026476557}, + {77.58068561553955, 12.94883346405509}, + {77.58019208908081, 12.948331575175299}, + {77.57991313934326, 12.943814529775414}, + {77.58497714996338, 12.94394000436408}, + {77.58517026901245, 12.9446301134728}, + {77.58572816848755, 12.945508431393435}, + {77.58785247802734, 12.946365833997325}, + {77.58967638015747, 12.946428570657417}, + {77.59070634841918, 12.947474179333993}, + {77.59317398071289, 12.948875288082773}, + {77.59167194366454, 12.949962710338657}, + {77.59077072143555, 12.950276388953625}, + {77.59098529815674, 12.951196510612728}, + {77.58729457855225, 12.952472128200755}, + {77.5853419303894, 12.953977766785052}, + }}} + doc := document.NewDocument("polygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon2 := [][][][]float64{{{ + {77.59527683258057, 12.951112863329588}, + {77.59420394897461, 12.947976069940545}, + {77.59579181671143, 12.946010325958518}, + {77.60347366333008, 12.950401860289055}, + {77.60673522949219, 12.95600618215462}, + {77.60107040405273, 12.96345053407734}, + {77.5984525680542, 12.961861309096507}, + {77.59527683258057, 12.951112863329588}, + }}} + doc = document.NewDocument("polygon2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon2, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon3 := [][][][]float64{{{ + {77.59974002838135, 12.953789562459688}, + {77.60347366333008, 12.953789562459688}, + {77.60347366333008, 12.957720912158363}, + {77.59974002838135, 12.957720912158363}, + {77.59974002838135, 12.953789562459688}, + }}} + doc = document.NewDocument("polygon3") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon3, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + /*polygon4 := [][][][]float64{{{{8.515305519104004, 47.392597129887}, + {8.514232635498047, 47.38896544894171}, {8.507537841796875, 47.38815191810328}, + {8.514318466186523, 47.38725120859953}, {8.516035079956053, 47.383357642070706}, + {8.516979217529295, 47.38733837470806}, {8.522472381591797, 47.38794853343167}, + {8.516507148742676, 47.388994503382285}, {8.515305519104004, 47.392597129887}}}} + doc = document.NewDocument("polygon4") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon4, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + }*/ + + // not working envelope + envelope1 := [][][][]float64{{{ + {36.18896484375, 49.9799293145682}, + {36.20613098144531, 49.99714673955337}, + }}} + doc = document.NewDocument("envelope1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + envelope1, "envelope", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + doc = document.NewDocument("circle1") + doc.AddField(document.NewGeoCircleFieldWithIndexingOptions("geometry", + []uint64{}, []float64{77.59253025054932, 12.955587953533424}, "900m", + document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring := [][][][]float64{{{ + {77.5841188430786, 12.957093573282744}, + {77.57776737213135, 12.952074805390097}, + }}} + doc = document.NewDocument("linestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring1 := [][][][]float64{{{ + {77.60188579559325, 12.982604078764705}, + {77.60557651519775, 12.987329508048184}, + }}} + doc = document.NewDocument("linestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring1, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + linestring3 := [][][][]float64{{{ + {8.51539134979248, 47.390592472948434}, + {8.520884513854979, 47.388006643417924}, + }}} + doc = document.NewDocument("linestring3") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + linestring3, "linestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring := [][][][]float64{{ + { + {77.57227420806883, 12.948687079902895}, + {77.57600784301758, 12.954165970968194}, + }, + {{77.5779390335083, 12.94471376293191}, {77.57218837738037, 12.948268838994263}}, + {{77.57781028747559, 12.951740217268595}, {77.5779390335083, 12.945006535817749}}, + }} + doc = document.NewDocument("multilinestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring1 := [][][][]float64{{ + { + {77.6015853881836, 12.990089451715061}, + {77.60476112365723, 12.987747683302153}, + }, + {{77.59875297546387, 12.988751301039581}, {77.59446144104004, 12.98197680263484}}, + {{77.60188579559325, 12.982604078764705}, {77.60557651519775, 12.987329508048184}}, + }} + doc = document.NewDocument("multilinestring2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring1, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipoint1 := [][][][]float64{{{ + {77.56618022918701, 12.958180959662695}, + {77.56407737731932, 12.951614746607163}, + {77.56922721862793, 12.956173473406446}, + }}} + doc = document.NewDocument("multipoint1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipoint1, "multipoint", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygonWithHole1 := [][][][]float64{{ + { + {77.59991168975829, 12.972232910164502}, + {77.6039457321167, 12.97582941279006}, + {77.60424613952637, 12.98168407323241}, + {77.59974002838135, 12.985489528568463}, + {77.59321689605713, 12.979300406693417}, + {77.59991168975829, 12.972232910164502}, + }, + { + {77.59682178497314, 12.975787593290978}, + {77.60295867919922, 12.975787593290978}, + {77.60295867919922, 12.98143316204164}, + {77.59682178497314, 12.98143316204164}, + {77.59682178497314, 12.975787593290978}, + }, + }} + + doc = document.NewDocument("polygonWithHole1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygonWithHole1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon4 := [][][][]float64{{{ + {-121.48125886917113, 38.51009442323401}, + {-121.48361921310425, 38.51012800441735}, + {-121.48497104644774, 38.50858325377352}, + {-121.48366212844849, 38.507861239391026}, + {-121.48353338241577, 38.50277335141579}, + {-121.4803147315979, 38.50267259752949}, + {-121.48033618927, 38.5046204810195}, + {-121.47771835327147, 38.50754220747402}, + {-121.48123741149902, 38.508616835661655}, + {-121.48125886917113, 38.51009442323401}, + }}} + + doc = document.NewDocument("polygon4") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon4, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipolygon1 := [][][][]float64{ + {{ + {-121.49104356765746, 38.52149433504263}, + {-121.47857666015625, 38.51592052417851}, + {-121.47688150405884, 38.515970891871696}, + {-121.4770746231079, 38.51714612804143}, + {-121.49033546447754, 38.52221621271097}, + {-121.49104356765746, 38.52149433504263}, + }}, + {{ + {-121.47647380828859, 38.51714612804143}, + {-121.47658109664916, 38.51477884701455}, + {-121.4741563796997, 38.5159876810949}, + {-121.47647380828859, 38.51714612804143}, + }}, + } + + doc = document.NewDocument("multipolygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipolygon1, "multipolygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} + +func TestGeoJsonMultiPolygonWithInQuery(t *testing.T) { + tests := []struct { + polygon [][][][]float64 + field string + want []string + }{ + // test within multipolygon query for multipolygon1. + // (where each query polygon contains each of the indexed polygons) + { + [][][][]float64{ + { + { + {-121.49458408355713, 38.53270780324851}, + {-121.48823261260985, 38.52533866992879}, + {-121.48048639297485, 38.53253994984147}, + {-121.49458408355713, 38.53270780324851}, + }, + }, + {{ + {-121.48700952529907, 38.53306029412857}, + {-121.48160219192505, 38.53306029412857}, + {-121.48160219192505, 38.53829709805414}, + {-121.48700952529907, 38.53829709805414}, + {-121.48700952529907, 38.53306029412857}, + }}, + {{ + {-121.47344827651976, 38.54475865436684}, + {-121.46396398544312, 38.54475865436684}, + {-121.46396398544312, 38.55366961462033}, + {-121.47344827651976, 38.55366961462033}, + {-121.47344827651976, 38.54475865436684}, + }}, + }, + "geometry", + []string{"multipolygon1"}, + }, + + // test within multipolygon query. (only partial containment of the three + // indexed polygons by the two query polygons) + { + [][][][]float64{ + { + { + {-121.49458408355713, 38.53270780324851}, + {-121.48823261260985, 38.52533866992879}, + {-121.48048639297485, 38.53253994984147}, + {-121.49458408355713, 38.53270780324851}, + }, + }, + {{ + {-121.48700952529907, 38.53306029412857}, + {-121.48160219192505, 38.53306029412857}, + {-121.48160219192505, 38.53829709805414}, + {-121.48700952529907, 38.53829709805414}, + {-121.48700952529907, 38.53306029412857}, + }}, + {{ + {-121.4734697341919, 38.544825784372485}, + {-121.4644145965576, 38.544825784372485}, + {-121.4644145965576, 38.5537199558913}, + {-121.4734697341919, 38.5537199558913}, + {-121.4734697341919, 38.544825784372485}, + }}, + }, + "geometry", nil, + }, + + // test within multipolygon query for multilinestring1. + {[][][][]float64{ + {{ + {-121.49876832962036, 38.551739839324334}, + {-121.49814605712889, 38.54553064564853}, + {-121.49158000946044, 38.54908841140355}, + {-121.49876832962036, 38.551739839324334}, + }}, + { + { + {-121.49258852005006, 38.54294612052762}, + {-121.49117231369017, 38.54294612052762}, + {-121.49117231369017, 38.54526212788182}, + {-121.49258852005006, 38.54526212788182}, + {-121.49258852005006, 38.54294612052762}, + }, + }, + }, "geometry", []string{"multilinestring1"}}, + + // test within multipolygon query for multipoint1. + {[][][][]float64{ + {{ + {-121.50286674499512, 38.564810956372185}, + {-121.49694442749023, 38.56226068115802}, + {-121.48406982421875, 38.5675624676039}, + {-121.4875030517578, 38.57514535565976}, + {-121.50286674499512, 38.564810956372185}, + }}, + {{ + {-121.48685932159422, 38.565163289911425}, + {-121.48623704910278, 38.56283114531348}, + {-121.48357629776001, 38.565129734410704}, + {-121.48685932159422, 38.565163289911425}, + }}, + { + { + {-121.49430513381958, 38.56195866888961}, + {-121.4899492263794, 38.5584518779682}, + {-121.48842573165892, 38.56194189039304}, + {-121.49430513381958, 38.56195866888961}, + }, + }, + }, "geometry", []string{"multipoint1"}}, + } + i := setupGeoJsonShapesIndexForMultiPolygonQuery(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapeMultiPolygonQueryWithRelation("within", + indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.polygon) + } + } +} + +func runGeoShapeMultiPolygonQueryWithRelation(relation string, + i index.IndexReader, + points [][][][]float64, field string, +) ([]string, error) { + var rv []string + s := geo.NewGeoJsonMultiPolygon(points) + + gbs, err := NewGeoShapeSearcher(context.TODO(), i, s, relation, + field, 1.0, search.SearcherOptions{}) + if err != nil { + return nil, err + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(gbs.DocumentMatchPoolSize(), 0), + } + docMatch, err := gbs.Next(ctx) + for docMatch != nil && err == nil { + docID, _ := i.ExternalID(docMatch.IndexInternalID) + rv = append(rv, docID) + docMatch, err = gbs.Next(ctx) + } + if err != nil { + return nil, err + } + return rv, nil +} + +func setupGeoJsonShapesIndexForMultiPolygonQuery(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + multipolygon1 := [][][][]float64{{{ + {-121.49140834808348, 38.5320028163074}, + {-121.49112939834593, 38.52916601331889}, + {-121.48889780044556, 38.52913244101627}, + {-121.4887261390686, 38.527655244193205}, + {-121.48559331893921, 38.52794061412457}, + {-121.48638725280762, 38.53213710006686}, + {-121.49140834808348, 38.5320028163074}, + }}, // polygon1 + {{ + {-121.48677349090575, 38.533194575914315}, + {-121.48179531097412, 38.533194575914315}, + {-121.48179531097412, 38.53814604174215}, + {-121.48677349090575, 38.53814604174215}, + {-121.48677349090575, 38.533194575914315}, + }}, // polygon2 + {{ + {-121.47334098815918, 38.553485029658475}, + {-121.47329807281494, 38.54485934935182}, + {-121.46415710449219, 38.54526212788182}, + {-121.47334098815918, 38.553485029658475}, + }}} + doc := document.NewDocument("multipolygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipolygon1, "multipolygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multilinestring1 := [][][][]float64{{ + { + {-121.4983820915222, 38.55081688500274}, + {-121.49649381637572, 38.550447699956685}, + }, + {{-121.49655818939209, 38.548635309508775}, {-121.49370431900023, 38.54811507788636}}, + {{-121.49134397506714, 38.54490969679143}, {-121.4919662475586, 38.54304681805045}}, + }} + doc = document.NewDocument("multilinestring1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multilinestring1, "multilinestring", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + multipoint1 := [][][][]float64{{{ + {-121.48960590362547, 38.56066671319285}, + {-121.4933180809021, 38.56157276247755}, + {-121.4973521232605, 38.56318348855919}, + {-121.48582935333252, 38.56736114108619}, + {-121.50104284286498, 38.56449217691959}, + {-121.4881682395935, 38.57158887950165}, + }}} + doc = document.NewDocument("multipoint1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + multipoint1, "multipoint", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} + +func setupGeoJsonPolygonS2LoopPortingIssue(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + polygon1 := [][][][]float64{{{ + {-135.0, -38.0}, + {149.0, -38.0}, + {149.0, 77.0}, + {-135.0, 77.0}, + }}} + doc := document.NewDocument("polygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + return i +} + +func TestGeoJsonPolygonContainsQueryS2LoopPortingIssue(t *testing.T) { + tests := []struct { + polygon [][][]float64 + field string + want []string + }{ + // test containment query polygon for polygon1. + { + [][][]float64{{ + {13.007812500000002, 37.99616267972809}, + {13.559375000000002, 37.99616267972809}, + {13.559375000000002, 38.472819658516866}, + {13.007812500000002, 38.472819658516866}, + }}, + "geometry", + []string{"polygon1"}, + }, + + // test containment query polygon for polygon1. + { + [][][]float64{{ + {13.007812500000002, 37.99616267972809}, + {13.359375000000002, 37.99616267972809}, + {13.359375000000002, 38.272819658516866}, + {13.007812500000002, 38.272819658516866}, + }}, + "geometry", + []string{"polygon1"}, + }, + } + i := setupGeoJsonPolygonS2LoopPortingIssue(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePolygonQueryWithRelation("contains", + indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", + n, test.want, got, test.polygon) + } + } +} + +func TestGeoJsonPolygonIntersectsQuery1(t *testing.T) { + tests := []struct { + polygon [][][]float64 + field string + want []string + }{ + // test non-intersecting query polygon. + {[][][]float64{{ + { + 97.745361328125, + 68.21644657802169, + }, + { + 97.701416015625, + 67.97051353559428, + }, + { + 97.80029296875, + 67.97875365614591, + }, + { + 97.745361328125, + 68.21644657802169, + }, + }}, "geometry", nil}, + + // test intersecting query polygon. + {[][][]float64{{ + { + 77.59214401245117, + 12.966043458314124, + }, + { + 77.58853912353516, + 12.95232574618635, + }, + { + 77.60943889617919, + 12.956466232826733, + }, + { + 77.59214401245117, + 12.966043458314124, + }, + }}, "geometry", nil}, + + // test intersecting query polygon for polygon1. + {[][][]float64{{ + {97.0806884765625, 61.61423180712503}, + {96.7510986328125, 61.54625879879804}, + {97.305908203125, 61.367777577924}, + {97.0806884765625, 61.61423180712503}, + }}, "geometry", []string{"polygon1"}}, + } + i := setupGeoJsonShapesIndexForPolygonQuery1(t) + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err = indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + for n, test := range tests { + got, err := runGeoShapePolygonQueryWithRelation("intersects", + indexReader, test.polygon, test.field) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("test %d, expected %v, got %v for polygon: %+v", n, + test.want, got, test.polygon) + } + } +} + +func setupGeoJsonShapesIndexForPolygonQuery1(t *testing.T) index.Index { + analysisQueue := index.NewAnalysisQueue(1) + i, err := scorch.NewScorch( + gtreap.Name, + map[string]interface{}{ + "path": "", + "spatialPlugin": "s2", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + + polygon1 := [][][][]float64{{{ + {96.69202458735312, 61.59480859768306}, + {96.79202458735311, 61.39480859768306}, + {96.79202458735311, 61.59480859768306}, + {96.69202458735312, 61.59480859768306}, + }}} + doc := document.NewDocument("polygon1") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon1, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + polygon2 := [][][][]float64{{{ + {91.35604953911839, 65.11164029408492}, + {91.45604953911838, 64.91164029408492}, + {91.45604953911838, 65.11164029408492}, + {91.35604953911839, 65.11164029408492}, + }}} + doc = document.NewDocument("polygon2") + doc.AddField(document.NewGeoShapeFieldWithIndexingOptions("geometry", []uint64{}, + polygon2, "polygon", document.DefaultGeoShapeIndexingOptions)) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + return i +} diff --git a/search/searcher/search_ip_range.go b/search/searcher/search_ip_range.go new file mode 100644 index 0000000..3826620 --- /dev/null +++ b/search/searcher/search_ip_range.go @@ -0,0 +1,68 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "net" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +// netLimits returns the lo and hi bounds inside the network. +func netLimits(n *net.IPNet) (lo net.IP, hi net.IP) { + ones, bits := n.Mask.Size() + netNum := n.IP + if bits == net.IPv4len*8 { + netNum = netNum.To16() + ones += 8 * (net.IPv6len - net.IPv4len) + } + mask := net.CIDRMask(ones, 8*net.IPv6len) + lo = make(net.IP, net.IPv6len) + hi = make(net.IP, net.IPv6len) + for i := 0; i < net.IPv6len; i++ { + lo[i] = netNum[i] & mask[i] + hi[i] = lo[i] | ^mask[i] + } + return lo, hi +} + +func NewIPRangeSearcher(ctx context.Context, indexReader index.IndexReader, ipNet *net.IPNet, + field string, boost float64, options search.SearcherOptions) ( + search.Searcher, error) { + + lo, hi := netLimits(ipNet) + fieldDict, err := indexReader.FieldDictRange(field, lo, hi) + if err != nil { + return nil, err + } + defer fieldDict.Close() + + var terms []string + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + terms = append(terms, tfd.Term) + if tooManyClauses(len(terms)) { + return nil, tooManyClausesErr(field, len(terms)) + } + tfd, err = fieldDict.Next() + } + if err != nil { + return nil, err + } + + return NewMultiTermSearcher(ctx, indexReader, terms, field, boost, options, true) +} diff --git a/search/searcher/search_ip_range_test.go b/search/searcher/search_ip_range_test.go new file mode 100644 index 0000000..7abbe1b --- /dev/null +++ b/search/searcher/search_ip_range_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2021 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "net" + "testing" +) + +func Test_netLimits(t *testing.T) { + tests := []struct { + arg string + lo string + hi string + }{ + {"128.0.0.0/1", "128.0.0.0", "255.255.255.255"}, + {"128.0.0.0/7", "128.0.0.0", "129.255.255.255"}, + {"1.1.1.1/8", "1.0.0.0", "1.255.255.255"}, + {"1.2.3.0/24", "1.2.3.0", "1.2.3.255"}, + {"1.2.2.0/23", "1.2.2.0", "1.2.3.255"}, + {"1.2.3.128/25", "1.2.3.128", "1.2.3.255"}, + {"1.2.3.0/25", "1.2.3.0", "1.2.3.127"}, + {"1.2.3.4/31", "1.2.3.4", "1.2.3.5"}, + {"1.2.3.4/32", "1.2.3.4", "1.2.3.4"}, + {"2a00:23c8:7283:ff00:1fa8:0:0:0/80", "2a00:23c8:7283:ff00:1fa8::", "2a00:23c8:7283:ff00:1fa8:ffff:ffff:ffff"}, + } + for _, tt := range tests { + t.Run(tt.arg, func(t *testing.T) { + _, net, err := net.ParseCIDR(tt.arg) + if err != nil { + t.Fatal(err) + } + lo, hi := netLimits(net) + if lo.String() != tt.lo || hi.String() != tt.hi { + t.Errorf("netLimits(%q) = %s %s, want %s %s", tt.arg, lo, hi, tt.lo, tt.hi) + } + + }) + } +} diff --git a/search/searcher/search_knn.go b/search/searcher/search_knn.go new file mode 100644 index 0000000..a95a714 --- /dev/null +++ b/search/searcher/search_knn.go @@ -0,0 +1,145 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package searcher + +import ( + "context" + "encoding/json" + "reflect" + + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeKNNSearcher int + +func init() { + var ks KNNSearcher + reflectStaticSizeKNNSearcher = int(reflect.TypeOf(ks).Size()) +} + +type KNNSearcher struct { + field string + vector []float32 + k int64 + indexReader index.IndexReader + vectorReader index.VectorReader + scorer *scorer.KNNQueryScorer + count uint64 + vd index.VectorDoc +} + +func NewKNNSearcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, + options search.SearcherOptions, field string, vector []float32, k int64, + boost float64, similarityMetric string, searchParams json.RawMessage, + eligibleSelector index.EligibleDocumentSelector) ( + search.Searcher, error) { + + if vr, ok := i.(index.VectorIndexReader); ok { + vectorReader, err := vr.VectorReader(ctx, vector, field, k, searchParams, eligibleSelector) + if err != nil { + return nil, err + } + knnScorer := scorer.NewKNNQueryScorer(vector, field, boost, + options, similarityMetric) + return &KNNSearcher{ + indexReader: i, + vectorReader: vectorReader, + field: field, + vector: vector, + k: k, + scorer: knnScorer, + }, nil + } + return nil, nil +} + +func (s *KNNSearcher) VectorOptimize(ctx context.Context, octx index.VectorOptimizableContext) ( + index.VectorOptimizableContext, error) { + o, ok := s.vectorReader.(index.VectorOptimizable) + if ok { + return o.VectorOptimize(ctx, octx) + } + + return nil, nil +} + +func (s *KNNSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) ( + *search.DocumentMatch, error) { + knnMatch, err := s.vectorReader.Next(s.vd.Reset()) + if err != nil { + return nil, err + } + + if knnMatch == nil { + return nil, nil + } + + docMatch := s.scorer.Score(ctx, knnMatch) + + return docMatch, nil +} + +func (s *KNNSearcher) Close() error { + return s.vectorReader.Close() +} + +func (s *KNNSearcher) Count() uint64 { + return s.vectorReader.Count() +} + +func (s *KNNSearcher) DocumentMatchPoolSize() int { + return 1 +} + +func (s *KNNSearcher) Min() int { + return 0 +} + +func (s *KNNSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + knnMatch, err := s.vectorReader.Next(s.vd.Reset()) + if err != nil { + return nil, err + } + + if knnMatch == nil { + return nil, nil + } + + docMatch := s.scorer.Score(ctx, knnMatch) + + return docMatch, nil +} + +func (s *KNNSearcher) SetQueryNorm(qnorm float64) { + s.scorer.SetQueryNorm(qnorm) +} + +func (s *KNNSearcher) Size() int { + return reflectStaticSizeKNNSearcher + size.SizeOfPtr + + s.vectorReader.Size() + + s.vd.Size() + + s.scorer.Size() +} + +func (s *KNNSearcher) Weight() float64 { + return s.scorer.Weight() +} diff --git a/search/searcher/search_match_all.go b/search/searcher/search_match_all.go new file mode 100644 index 0000000..57d8d07 --- /dev/null +++ b/search/searcher/search_match_all.go @@ -0,0 +1,123 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeMatchAllSearcher int + +func init() { + var mas MatchAllSearcher + reflectStaticSizeMatchAllSearcher = int(reflect.TypeOf(mas).Size()) +} + +type MatchAllSearcher struct { + indexReader index.IndexReader + reader index.DocIDReader + scorer *scorer.ConstantScorer + count uint64 +} + +func NewMatchAllSearcher(ctx context.Context, indexReader index.IndexReader, boost float64, options search.SearcherOptions) (*MatchAllSearcher, error) { + reader, err := indexReader.DocIDReaderAll() + if err != nil { + return nil, err + } + count, err := indexReader.DocCount() + if err != nil { + _ = reader.Close() + return nil, err + } + scorer := scorer.NewConstantScorer(1.0, boost, options) + + return &MatchAllSearcher{ + indexReader: indexReader, + reader: reader, + scorer: scorer, + count: count, + }, nil +} + +func (s *MatchAllSearcher) Size() int { + return reflectStaticSizeMatchAllSearcher + size.SizeOfPtr + + s.reader.Size() + + s.scorer.Size() +} + +func (s *MatchAllSearcher) Count() uint64 { + return s.count +} + +func (s *MatchAllSearcher) Weight() float64 { + return s.scorer.Weight() +} + +func (s *MatchAllSearcher) SetQueryNorm(qnorm float64) { + s.scorer.SetQueryNorm(qnorm) +} + +func (s *MatchAllSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + id, err := s.reader.Next() + if err != nil { + return nil, err + } + + if id == nil { + return nil, nil + } + + // score match + docMatch := s.scorer.Score(ctx, id) + // return doc match + return docMatch, nil + +} + +func (s *MatchAllSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + id, err := s.reader.Advance(ID) + if err != nil { + return nil, err + } + + if id == nil { + return nil, nil + } + + // score match + docMatch := s.scorer.Score(ctx, id) + + // return doc match + return docMatch, nil +} + +func (s *MatchAllSearcher) Close() error { + return s.reader.Close() +} + +func (s *MatchAllSearcher) Min() int { + return 0 +} + +func (s *MatchAllSearcher) DocumentMatchPoolSize() int { + return 1 +} diff --git a/search/searcher/search_match_all_test.go b/search/searcher/search_match_all_test.go new file mode 100644 index 0000000..f3b3d88 --- /dev/null +++ b/search/searcher/search_match_all_test.go @@ -0,0 +1,146 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestMatchAllSearch(t *testing.T) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + explainTrue := search.SearcherOptions{Explain: true} + + allSearcher, err := NewMatchAllSearcher(context.TODO(), twoDocIndexReader, 1.0, explainTrue) + if err != nil { + t.Fatal(err) + } + + allSearcher2, err := NewMatchAllSearcher(context.TODO(), twoDocIndexReader, 1.2, explainTrue) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + searcher search.Searcher + queryNorm float64 + results []*search.DocumentMatch + }{ + { + searcher: allSearcher, + queryNorm: 1.0, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("2"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("4"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("5"), + Score: 1.0, + }, + }, + }, + { + searcher: allSearcher2, + queryNorm: 0.8333333, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("1"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("2"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("3"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("4"), + Score: 1.0, + }, + { + IndexInternalID: index.IndexInternalID("5"), + Score: 1.0, + }, + }, + }, + } + + for testIndex, test := range tests { + + if test.queryNorm != 1.0 { + test.searcher.SetQueryNorm(test.queryNorm) + } + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(test.searcher.DocumentMatchPoolSize(), 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + if i < len(test.results) { + if !next.IndexInternalID.Equals(test.results[i].IndexInternalID) { + t.Errorf("expected result %d to have id %s got %s for test %d", i, test.results[i].IndexInternalID, next.IndexInternalID, testIndex) + } + if !scoresCloseEnough(next.Score, test.results[i].Score) { + t.Errorf("expected result %d to have score %v got %v for test %d", i, test.results[i].Score, next.Score, testIndex) + t.Logf("scoring explanation: %s", next.Expl) + } + } + ctx.DocumentMatchPool.Put(next) + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.results) != i { + t.Errorf("expected %d results got %d for test %d", len(test.results), i, testIndex) + } + } +} diff --git a/search/searcher/search_match_none.go b/search/searcher/search_match_none.go new file mode 100644 index 0000000..b7f7694 --- /dev/null +++ b/search/searcher/search_match_none.go @@ -0,0 +1,76 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeMatchNoneSearcher int + +func init() { + var mns MatchNoneSearcher + reflectStaticSizeMatchNoneSearcher = int(reflect.TypeOf(mns).Size()) +} + +type MatchNoneSearcher struct { + indexReader index.IndexReader +} + +func NewMatchNoneSearcher(indexReader index.IndexReader) (*MatchNoneSearcher, error) { + return &MatchNoneSearcher{ + indexReader: indexReader, + }, nil +} + +func (s *MatchNoneSearcher) Size() int { + return reflectStaticSizeMatchNoneSearcher + size.SizeOfPtr +} + +func (s *MatchNoneSearcher) Count() uint64 { + return uint64(0) +} + +func (s *MatchNoneSearcher) Weight() float64 { + return 0.0 +} + +func (s *MatchNoneSearcher) SetQueryNorm(qnorm float64) { + +} + +func (s *MatchNoneSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + return nil, nil +} + +func (s *MatchNoneSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + return nil, nil +} + +func (s *MatchNoneSearcher) Close() error { + return nil +} + +func (s *MatchNoneSearcher) Min() int { + return 0 +} + +func (s *MatchNoneSearcher) DocumentMatchPoolSize() int { + return 0 +} diff --git a/search/searcher/search_match_none_test.go b/search/searcher/search_match_none_test.go new file mode 100644 index 0000000..c3a370d --- /dev/null +++ b/search/searcher/search_match_none_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "testing" + + "github.com/blevesearch/bleve/v2/search" +) + +func TestMatchNoneSearch(t *testing.T) { + + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + noneSearcher, err := NewMatchNoneSearcher(twoDocIndexReader) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + searcher search.Searcher + results []*search.DocumentMatch + }{ + { + searcher: noneSearcher, + results: []*search.DocumentMatch{}, + }, + } + + for testIndex, test := range tests { + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(test.searcher.DocumentMatchPoolSize(), 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + if i < len(test.results) { + if !next.IndexInternalID.Equals(test.results[i].IndexInternalID) { + t.Errorf("expected result %d to have id %s got %s for test %d", i, test.results[i].IndexInternalID, next.IndexInternalID, testIndex) + } + if !scoresCloseEnough(next.Score, test.results[i].Score) { + t.Errorf("expected result %d to have score %v got %v for test %d", i, test.results[i].Score, next.Score, testIndex) + t.Logf("scoring explanation: %s", next.Expl) + } + } + ctx.DocumentMatchPool.Put(next) + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.results) != i { + t.Errorf("expected %d results got %d for test %d", len(test.results), i, testIndex) + } + } +} diff --git a/search/searcher/search_multi_term.go b/search/searcher/search_multi_term.go new file mode 100644 index 0000000..98f8f92 --- /dev/null +++ b/search/searcher/search_multi_term.go @@ -0,0 +1,268 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "fmt" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func NewMultiTermSearcher(ctx context.Context, indexReader index.IndexReader, terms []string, + field string, boost float64, options search.SearcherOptions, limit bool) ( + search.Searcher, error) { + + if tooManyClauses(len(terms)) { + if optionsDisjunctionOptimizable(options) { + return optimizeMultiTermSearcher(ctx, indexReader, terms, field, boost, options) + } + if limit { + return nil, tooManyClausesErr(field, len(terms)) + } + } + + qsearchers, err := makeBatchSearchers(ctx, indexReader, terms, field, boost, options) + if err != nil { + return nil, err + } + + // build disjunction searcher of these ranges + return newMultiTermSearcherInternal(ctx, indexReader, qsearchers, field, boost, + options, limit) +} + +// Works similarly to the multi term searcher but additionally boosts individual terms based on +// their edit distance from the query terms +func NewMultiTermSearcherBoosted(ctx context.Context, indexReader index.IndexReader, terms []string, + field string, boost float64, editDistances []uint8, options search.SearcherOptions, limit bool) ( + search.Searcher, error) { + + if tooManyClauses(len(terms)) { + if optionsDisjunctionOptimizable(options) { + return optimizeMultiTermSearcher(ctx, indexReader, terms, field, boost, options) + } + if limit { + return nil, tooManyClausesErr(field, len(terms)) + } + } + + qsearchers, err := makeBatchSearchersBoosted(ctx, indexReader, terms, field, boost, editDistances, options) + if err != nil { + return nil, err + } + + // build disjunction searcher of these ranges + return newMultiTermSearcherInternal(ctx, indexReader, qsearchers, field, boost, + options, limit) +} + +func NewMultiTermSearcherBytes(ctx context.Context, indexReader index.IndexReader, terms [][]byte, + field string, boost float64, options search.SearcherOptions, limit bool) ( + search.Searcher, error) { + + if tooManyClauses(len(terms)) { + if optionsDisjunctionOptimizable(options) { + return optimizeMultiTermSearcherBytes(ctx, indexReader, terms, field, boost, options) + } + + if limit { + return nil, tooManyClausesErr(field, len(terms)) + } + } + + qsearchers, err := makeBatchSearchersBytes(ctx, indexReader, terms, field, boost, options) + if err != nil { + return nil, err + } + + // build disjunction searcher of these ranges + return newMultiTermSearcherInternal(ctx, indexReader, qsearchers, field, boost, + options, limit) +} + +func newMultiTermSearcherInternal(ctx context.Context, indexReader index.IndexReader, + searchers []search.Searcher, field string, boost float64, + options search.SearcherOptions, limit bool) ( + search.Searcher, error) { + + // build disjunction searcher of these ranges + searcher, err := newDisjunctionSearcher(ctx, indexReader, searchers, 0, options, + limit) + if err != nil { + for _, s := range searchers { + _ = s.Close() + } + return nil, err + } + + return searcher, nil +} + +func optimizeMultiTermSearcher(ctx context.Context, indexReader index.IndexReader, terms []string, + field string, boost float64, options search.SearcherOptions) ( + search.Searcher, error) { + var finalSearcher search.Searcher + for len(terms) > 0 { + var batchTerms []string + if len(terms) > DisjunctionMaxClauseCount { + batchTerms = terms[:DisjunctionMaxClauseCount] + terms = terms[DisjunctionMaxClauseCount:] + } else { + batchTerms = terms + terms = nil + } + batch, err := makeBatchSearchers(ctx, indexReader, batchTerms, field, boost, options) + if err != nil { + return nil, err + } + if finalSearcher != nil { + batch = append(batch, finalSearcher) + } + cleanup := func() { + for _, searcher := range batch { + if searcher != nil { + _ = searcher.Close() + } + } + } + finalSearcher, err = optimizeCompositeSearcher(ctx, "disjunction:unadorned", + indexReader, batch, options) + // all searchers in batch should be closed, regardless of error or optimization failure + // either we're returning, or continuing and only finalSearcher is needed for next loop + cleanup() + if err != nil { + return nil, err + } + if finalSearcher == nil { + return nil, fmt.Errorf("unable to optimize") + } + } + return finalSearcher, nil +} + +func makeBatchSearchers(ctx context.Context, indexReader index.IndexReader, terms []string, field string, + boost float64, options search.SearcherOptions) ([]search.Searcher, error) { + + qsearchers := make([]search.Searcher, len(terms)) + qsearchersClose := func() { + for _, searcher := range qsearchers { + if searcher != nil { + _ = searcher.Close() + } + } + } + for i, term := range terms { + var err error + qsearchers[i], err = NewTermSearcher(ctx, indexReader, term, field, boost, options) + if err != nil { + qsearchersClose() + return nil, err + } + } + return qsearchers, nil +} + +func makeBatchSearchersBoosted(ctx context.Context, indexReader index.IndexReader, terms []string, field string, + boost float64, editDistances []uint8, options search.SearcherOptions) ([]search.Searcher, error) { + + qsearchers := make([]search.Searcher, len(terms)) + qsearchersClose := func() { + for _, searcher := range qsearchers { + if searcher != nil { + _ = searcher.Close() + } + } + } + for i, term := range terms { + var err error + var editMultiplier float64 + if editDistances != nil { + editMultiplier = 1 / float64(editDistances[i]+1) + } + qsearchers[i], err = NewTermSearcher(ctx, indexReader, term, field, boost*editMultiplier, options) + if err != nil { + qsearchersClose() + return nil, err + } + } + return qsearchers, nil +} + +func optimizeMultiTermSearcherBytes(ctx context.Context, indexReader index.IndexReader, terms [][]byte, + field string, boost float64, options search.SearcherOptions) ( + search.Searcher, error) { + + var finalSearcher search.Searcher + for len(terms) > 0 { + var batchTerms [][]byte + if len(terms) > DisjunctionMaxClauseCount { + batchTerms = terms[:DisjunctionMaxClauseCount] + terms = terms[DisjunctionMaxClauseCount:] + } else { + batchTerms = terms + terms = nil + } + batch, err := makeBatchSearchersBytes(ctx, indexReader, batchTerms, field, boost, options) + if err != nil { + return nil, err + } + if finalSearcher != nil { + batch = append(batch, finalSearcher) + } + cleanup := func() { + for _, searcher := range batch { + if searcher != nil { + _ = searcher.Close() + } + } + } + finalSearcher, err = optimizeCompositeSearcher(ctx, "disjunction:unadorned", + indexReader, batch, options) + // all searchers in batch should be closed, regardless of error or optimization failure + // either we're returning, or continuing and only finalSearcher is needed for next loop + cleanup() + if err != nil { + return nil, err + } + if finalSearcher == nil { + return nil, fmt.Errorf("unable to optimize") + } + } + return finalSearcher, nil +} + +func makeBatchSearchersBytes(ctx context.Context, indexReader index.IndexReader, terms [][]byte, field string, + boost float64, options search.SearcherOptions) ([]search.Searcher, error) { + + qsearchers := make([]search.Searcher, len(terms)) + qsearchersClose := func() { + for _, searcher := range qsearchers { + if searcher != nil { + _ = searcher.Close() + } + } + } + for i, term := range terms { + var err error + qsearchers[i], err = NewTermSearcherBytes(ctx, indexReader, term, field, boost, options) + if err != nil { + qsearchersClose() + return nil, err + } + } + return qsearchers, nil +} diff --git a/search/searcher/search_numeric_range.go b/search/searcher/search_numeric_range.go new file mode 100644 index 0000000..f086051 --- /dev/null +++ b/search/searcher/search_numeric_range.go @@ -0,0 +1,258 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "bytes" + "context" + "math" + "sort" + + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func NewNumericRangeSearcher(ctx context.Context, indexReader index.IndexReader, + min *float64, max *float64, inclusiveMin, inclusiveMax *bool, field string, + boost float64, options search.SearcherOptions) (search.Searcher, error) { + // account for unbounded edges + if min == nil { + negInf := math.Inf(-1) + min = &negInf + } + if max == nil { + Inf := math.Inf(1) + max = &Inf + } + if inclusiveMin == nil { + defaultInclusiveMin := true + inclusiveMin = &defaultInclusiveMin + } + if inclusiveMax == nil { + defaultInclusiveMax := false + inclusiveMax = &defaultInclusiveMax + } + // find all the ranges + minInt64 := numeric.Float64ToInt64(*min) + if !*inclusiveMin && minInt64 != math.MaxInt64 { + minInt64++ + } + maxInt64 := numeric.Float64ToInt64(*max) + if !*inclusiveMax && maxInt64 != math.MinInt64 { + maxInt64-- + } + + var fieldDict index.FieldDictContains + var dictBytesRead uint64 + var isIndexed filterFunc + var err error + if irr, ok := indexReader.(index.IndexReaderContains); ok { + fieldDict, err = irr.FieldDictContains(field) + if err != nil { + return nil, err + } + + isIndexed = func(term []byte) bool { + found, err := fieldDict.Contains(term) + return err == nil && found + } + + dictBytesRead = fieldDict.BytesRead() + } + + // FIXME hard-coded precision, should match field declaration + termRanges := splitInt64Range(minInt64, maxInt64, 4) + terms := termRanges.Enumerate(isIndexed) + if fieldDict != nil { + if fd, ok := fieldDict.(index.FieldDict); ok { + if err = fd.Close(); err != nil { + return nil, err + } + } + } + + if len(terms) < 1 { + // reporting back the IO stats with respect to the dictionary + // loaded, using the context + if ctx != nil { + reportIOStats(ctx, dictBytesRead) + search.RecordSearchCost(ctx, search.AddM, dictBytesRead) + } + + // cannot return MatchNoneSearcher because of interaction with + // commit f391b991c20f02681bacd197afc6d8aed444e132 + return NewMultiTermSearcherBytes(ctx, indexReader, terms, field, + boost, options, true) + } + + // for upside_down + if isIndexed == nil { + terms, err = filterCandidateTerms(indexReader, terms, field) + if err != nil { + return nil, err + } + } + + if tooManyClauses(len(terms)) { + return nil, tooManyClausesErr(field, len(terms)) + } + + if ctx != nil { + reportIOStats(ctx, dictBytesRead) + search.RecordSearchCost(ctx, search.AddM, dictBytesRead) + } + + return NewMultiTermSearcherBytes(ctx, indexReader, terms, field, + boost, options, true) +} + +func filterCandidateTerms(indexReader index.IndexReader, + terms [][]byte, field string) (rv [][]byte, err error) { + + fieldDict, err := indexReader.FieldDictRange(field, terms[0], terms[len(terms)-1]) + if err != nil { + return nil, err + } + + // enumerate the terms and check against list of terms + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + termBytes := []byte(tfd.Term) + i := sort.Search(len(terms), func(i int) bool { return bytes.Compare(terms[i], termBytes) >= 0 }) + if i < len(terms) && bytes.Compare(terms[i], termBytes) == 0 { + rv = append(rv, terms[i]) + } + terms = terms[i:] + tfd, err = fieldDict.Next() + } + + if cerr := fieldDict.Close(); cerr != nil && err == nil { + err = cerr + } + + return rv, err +} + +type termRange struct { + startTerm []byte + endTerm []byte +} + +func (t *termRange) Enumerate(filter filterFunc) [][]byte { + var rv [][]byte + next := t.startTerm + for bytes.Compare(next, t.endTerm) <= 0 { + if filter != nil { + if filter(next) { + rv = append(rv, next) + } + } else { + rv = append(rv, next) + } + next = incrementBytes(next) + } + return rv +} + +func incrementBytes(in []byte) []byte { + rv := make([]byte, len(in)) + copy(rv, in) + for i := len(rv) - 1; i >= 0; i-- { + rv[i] = rv[i] + 1 + if rv[i] != 0 { + // didn't overflow, so stop + break + } + } + return rv +} + +type termRanges []*termRange + +func (tr termRanges) Enumerate(filter filterFunc) [][]byte { + var rv [][]byte + for _, tri := range tr { + trie := tri.Enumerate(filter) + rv = append(rv, trie...) + } + return rv +} + +func splitInt64Range(minBound, maxBound int64, precisionStep uint) termRanges { + rv := make(termRanges, 0) + if minBound > maxBound { + return rv + } + + for shift := uint(0); ; shift += precisionStep { + + diff := int64(1) << (shift + precisionStep) + mask := ((int64(1) << precisionStep) - int64(1)) << shift + hasLower := (minBound & mask) != int64(0) + hasUpper := (maxBound & mask) != mask + + var nextMinBound int64 + if hasLower { + nextMinBound = (minBound + diff) &^ mask + } else { + nextMinBound = minBound &^ mask + } + var nextMaxBound int64 + if hasUpper { + nextMaxBound = (maxBound - diff) &^ mask + } else { + nextMaxBound = maxBound &^ mask + } + + lowerWrapped := nextMinBound < minBound + upperWrapped := nextMaxBound > maxBound + + if shift+precisionStep >= 64 || nextMinBound > nextMaxBound || + lowerWrapped || upperWrapped { + // We are in the lowest precision or the next precision is not available. + rv = append(rv, newRange(minBound, maxBound, shift)) + // exit the split recursion loop + break + } + + if hasLower { + rv = append(rv, newRange(minBound, minBound|mask, shift)) + } + if hasUpper { + rv = append(rv, newRange(maxBound&^mask, maxBound, shift)) + } + + // recurse to next precision + minBound = nextMinBound + maxBound = nextMaxBound + } + + return rv +} + +func newRange(minBound, maxBound int64, shift uint) *termRange { + maxBound |= (int64(1) << shift) - int64(1) + minBytes := numeric.MustNewPrefixCodedInt64(minBound, shift) + maxBytes := numeric.MustNewPrefixCodedInt64(maxBound, shift) + return newRangeBytes(minBytes, maxBytes) +} + +func newRangeBytes(minBytes, maxBytes []byte) *termRange { + return &termRange{ + startTerm: minBytes, + endTerm: maxBytes, + } +} diff --git a/search/searcher/search_numeric_range_test.go b/search/searcher/search_numeric_range_test.go new file mode 100644 index 0000000..83611cd --- /dev/null +++ b/search/searcher/search_numeric_range_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/numeric" +) + +func TestSplitRange(t *testing.T) { + min := numeric.Float64ToInt64(1.0) + max := numeric.Float64ToInt64(5.0) + ranges := splitInt64Range(min, max, 4) + enumerated := ranges.Enumerate(nil) + if len(enumerated) != 135 { + t.Errorf("expected 135 terms, got %d", len(enumerated)) + } + +} + +func TestIncrementBytes(t *testing.T) { + tests := []struct { + in []byte + out []byte + }{ + { + in: []byte{0}, + out: []byte{1}, + }, + { + in: []byte{0, 0}, + out: []byte{0, 1}, + }, + { + in: []byte{0, 255}, + out: []byte{1, 0}, + }, + } + + for _, test := range tests { + actual := incrementBytes(test.in) + if !reflect.DeepEqual(actual, test.out) { + t.Errorf("expected %#v, got %#v", test.out, actual) + } + } +} diff --git a/search/searcher/search_phrase.go b/search/searcher/search_phrase.go new file mode 100644 index 0000000..07675cf --- /dev/null +++ b/search/searcher/search_phrase.go @@ -0,0 +1,554 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "fmt" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizePhraseSearcher int + +func init() { + var ps PhraseSearcher + reflectStaticSizePhraseSearcher = int(reflect.TypeOf(ps).Size()) +} + +type PhraseSearcher struct { + mustSearcher search.Searcher + queryNorm float64 + currMust *search.DocumentMatch + terms [][]string + path phrasePath + paths []phrasePath + locations []search.Location + initialized bool + // map a term to a list of fuzzy terms that match it + fuzzyTermMatches map[string][]string +} + +func (s *PhraseSearcher) Size() int { + sizeInBytes := reflectStaticSizePhraseSearcher + size.SizeOfPtr + + if s.mustSearcher != nil { + sizeInBytes += s.mustSearcher.Size() + } + + if s.currMust != nil { + sizeInBytes += s.currMust.Size() + } + + for _, entry := range s.terms { + sizeInBytes += size.SizeOfSlice + for _, entry1 := range entry { + sizeInBytes += size.SizeOfString + len(entry1) + } + } + + return sizeInBytes +} + +func NewPhraseSearcher(ctx context.Context, indexReader index.IndexReader, terms []string, + fuzziness int, autoFuzzy bool, field string, boost float64, options search.SearcherOptions) (*PhraseSearcher, error) { + + // turn flat terms []string into [][]string + mterms := make([][]string, len(terms)) + for i, term := range terms { + mterms[i] = []string{term} + } + return NewMultiPhraseSearcher(ctx, indexReader, mterms, fuzziness, autoFuzzy, field, boost, options) +} + +func NewMultiPhraseSearcher(ctx context.Context, indexReader index.IndexReader, terms [][]string, + fuzziness int, autoFuzzy bool, field string, boost float64, options search.SearcherOptions) (*PhraseSearcher, error) { + + options.IncludeTermVectors = true + var termPositionSearchers []search.Searcher + var err error + var ts search.Searcher + // The following logic checks if fuzziness is enabled. + // Fuzziness is considered enabled if either: + // a. `fuzziness` is greater than 0, or + // b. `autoFuzzy` is set to true. + // if both conditions are true, `autoFuzzy` takes precedence. + // If enabled, a map will be created to store the matches for fuzzy terms. + fuzzinessEnabled := autoFuzzy || fuzziness > 0 + var fuzzyTermMatches map[string][]string + if fuzzinessEnabled { + fuzzyTermMatches = make(map[string][]string) + ctx = context.WithValue(ctx, search.FuzzyMatchPhraseKey, fuzzyTermMatches) + } + // in case of fuzzy multi-phrase, phrase and match-phrase queries we hardcode the + // prefix length to 0, as setting a per word matching prefix length would not + // make sense from a user perspective. + for _, termPos := range terms { + if len(termPos) == 1 && termPos[0] != "" { + // single term + if fuzzinessEnabled { + // fuzzy + if autoFuzzy { + // auto fuzzy + ts, err = NewAutoFuzzySearcher(ctx, indexReader, termPos[0], 0, field, boost, options) + } else { + // non-auto fuzzy + ts, err = NewFuzzySearcher(ctx, indexReader, termPos[0], 0, fuzziness, field, boost, options) + } + } else { + // non-fuzzy + ts, err = NewTermSearcher(ctx, indexReader, termPos[0], field, boost, options) + } + if err != nil { + // close any searchers already opened + for _, ts := range termPositionSearchers { + _ = ts.Close() + } + return nil, fmt.Errorf("phrase searcher error building term searcher: %v", err) + } + termPositionSearchers = append(termPositionSearchers, ts) + } else if len(termPos) > 1 { + // multiple terms + var termSearchers []search.Searcher + for _, term := range termPos { + if term == "" { + continue + } + if fuzzinessEnabled { + // fuzzy + if autoFuzzy { + // auto fuzzy + ts, err = NewAutoFuzzySearcher(ctx, indexReader, term, 0, field, boost, options) + } else { + // non-auto fuzzy + ts, err = NewFuzzySearcher(ctx, indexReader, term, 0, fuzziness, field, boost, options) + } + } else { + // non-fuzzy + ts, err = NewTermSearcher(ctx, indexReader, term, field, boost, options) + } + if err != nil { + // close any searchers already opened + for _, ts := range termPositionSearchers { + _ = ts.Close() + } + return nil, fmt.Errorf("phrase searcher error building term searcher: %v", err) + } + termSearchers = append(termSearchers, ts) + } + disjunction, err := NewDisjunctionSearcher(ctx, indexReader, termSearchers, 1, options) + if err != nil { + // close any searchers already opened + for _, ts := range termPositionSearchers { + _ = ts.Close() + } + return nil, fmt.Errorf("phrase searcher error building term position disjunction searcher: %v", err) + } + termPositionSearchers = append(termPositionSearchers, disjunction) + } + } + + if ctx != nil { + if fts, ok := ctx.Value(search.FieldTermSynonymMapKey).(search.FieldTermSynonymMap); ok { + if ts, exists := fts[field]; exists { + if fuzzinessEnabled { + for term, fuzzyTerms := range fuzzyTermMatches { + fuzzySynonymTerms := make([]string, 0, len(fuzzyTerms)) + if s, found := ts[term]; found { + fuzzySynonymTerms = append(fuzzySynonymTerms, s...) + } + for _, fuzzyTerm := range fuzzyTerms { + if fuzzyTerm == term { + continue + } + if s, found := ts[fuzzyTerm]; found { + fuzzySynonymTerms = append(fuzzySynonymTerms, s...) + } + } + if len(fuzzySynonymTerms) > 0 { + fuzzyTermMatches[term] = append(fuzzyTermMatches[term], fuzzySynonymTerms...) + } + } + } else { + for _, termPos := range terms { + for _, term := range termPos { + if s, found := ts[term]; found { + if fuzzyTermMatches == nil { + fuzzyTermMatches = make(map[string][]string) + } + fuzzyTermMatches[term] = s + } + } + } + } + } + } + } + mustSearcher, err := NewConjunctionSearcher(ctx, indexReader, termPositionSearchers, options) + if err != nil { + // close any searchers already opened + for _, ts := range termPositionSearchers { + _ = ts.Close() + } + return nil, fmt.Errorf("phrase searcher error building conjunction searcher: %v", err) + } + + // build our searcher + rv := PhraseSearcher{ + mustSearcher: mustSearcher, + terms: terms, + fuzzyTermMatches: fuzzyTermMatches, + } + rv.computeQueryNorm() + return &rv, nil +} + +func (s *PhraseSearcher) computeQueryNorm() { + // first calculate sum of squared weights + sumOfSquaredWeights := 0.0 + if s.mustSearcher != nil { + sumOfSquaredWeights += s.mustSearcher.Weight() + } + + // now compute query norm from this + s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights) + // finally tell all the downstream searchers the norm + if s.mustSearcher != nil { + s.mustSearcher.SetQueryNorm(s.queryNorm) + } +} + +func (s *PhraseSearcher) initSearchers(ctx *search.SearchContext) error { + err := s.advanceNextMust(ctx) + if err != nil { + return err + } + + s.initialized = true + return nil +} + +func (s *PhraseSearcher) advanceNextMust(ctx *search.SearchContext) error { + var err error + + if s.mustSearcher != nil { + if s.currMust != nil { + ctx.DocumentMatchPool.Put(s.currMust) + } + s.currMust, err = s.mustSearcher.Next(ctx) + if err != nil { + return err + } + } + + return nil +} + +func (s *PhraseSearcher) Weight() float64 { + return s.mustSearcher.Weight() +} + +func (s *PhraseSearcher) SetQueryNorm(qnorm float64) { + s.mustSearcher.SetQueryNorm(qnorm) +} + +func (s *PhraseSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + + for s.currMust != nil { + // check this match against phrase constraints + rv := s.checkCurrMustMatch(ctx) + + // prepare for next iteration (either loop or subsequent call to Next()) + err := s.advanceNextMust(ctx) + if err != nil { + return nil, err + } + + // if match satisfied phrase constraints return it as a hit + if rv != nil { + return rv, nil + } + } + + return nil, nil +} + +// checkCurrMustMatch is solely concerned with determining if the DocumentMatch +// pointed to by s.currMust (which satisifies the pre-condition searcher) +// also satisfies the phrase constraints. if so, it returns a DocumentMatch +// for this document, otherwise nil +func (s *PhraseSearcher) checkCurrMustMatch(ctx *search.SearchContext) *search.DocumentMatch { + s.locations = s.currMust.Complete(s.locations) + + locations := s.currMust.Locations + s.currMust.Locations = nil + + ftls := s.currMust.FieldTermLocations + + // typically we would expect there to only actually be results in + // one field, but we allow for this to not be the case + // but, we note that phrase constraints can only be satisfied within + // a single field, so we can check them each independently + for field, tlm := range locations { + ftls = s.checkCurrMustMatchField(ctx, field, tlm, ftls) + } + + if len(ftls) > 0 { + // return match + rv := s.currMust + s.currMust = nil + rv.FieldTermLocations = ftls + return rv + } + + return nil +} + +// checkCurrMustMatchField is solely concerned with determining if one +// particular field within the currMust DocumentMatch Locations +// satisfies the phrase constraints (possibly more than once). if so, +// the matching field term locations are appended to the provided +// slice +func (s *PhraseSearcher) checkCurrMustMatchField(ctx *search.SearchContext, + field string, tlm search.TermLocationMap, + ftls []search.FieldTermLocation) []search.FieldTermLocation { + if s.path == nil { + s.path = make(phrasePath, 0, len(s.terms)) + } + var tlmPtr *search.TermLocationMap = &tlm + if s.fuzzyTermMatches != nil { + // if fuzzy search, we need to expand the tlm to include all the fuzzy matches + // Example - term is "foo" and fuzzy matches are "foo", "fool", "food" + // the non expanded tlm will be: + // foo -> Locations[foo] + // fool -> Locations[fool] + // food -> Locations[food] + // the expanded tlm will be: + // foo -> [Locations[foo], Locations[fool], Locations[food]] + expandedTlm := make(search.TermLocationMap) + s.expandFuzzyMatches(tlm, expandedTlm) + tlmPtr = &expandedTlm + } + s.paths = findPhrasePaths(0, nil, s.terms, *tlmPtr, s.path[:0], 0, s.paths[:0]) + for _, p := range s.paths { + for _, pp := range p { + ftls = append(ftls, search.FieldTermLocation{ + Field: field, + Term: pp.term, + Location: search.Location{ + Pos: pp.loc.Pos, + Start: pp.loc.Start, + End: pp.loc.End, + ArrayPositions: pp.loc.ArrayPositions, + }, + }) + } + } + return ftls +} + +func (s *PhraseSearcher) expandFuzzyMatches(tlm search.TermLocationMap, expandedTlm search.TermLocationMap) { + for term, fuzzyMatches := range s.fuzzyTermMatches { + locations := tlm[term] + for _, fuzzyMatch := range fuzzyMatches { + if fuzzyMatch == term { + continue + } + locations = append(locations, tlm[fuzzyMatch]...) + } + expandedTlm[term] = locations + } +} + +type phrasePart struct { + term string + loc *search.Location +} + +func (p *phrasePart) String() string { + return fmt.Sprintf("[%s %v]", p.term, p.loc) +} + +type phrasePath []phrasePart + +func (p phrasePath) MergeInto(in search.TermLocationMap) { + for _, pp := range p { + in[pp.term] = append(in[pp.term], pp.loc) + } +} + +func (p phrasePath) String() string { + rv := "[" + for i, pp := range p { + if i > 0 { + rv += ", " + } + rv += pp.String() + } + rv += "]" + return rv +} + +// findPhrasePaths is a function to identify phrase matches from a set +// of known term locations. it recursive so care must be taken with +// arguments and return values. +// +// prevPos - the previous location, 0 on first invocation +// +// ap - array positions of the first candidate phrase part to +// which further recursive phrase parts must match, +// nil on initial invocation or when there are no array positions +// +// phraseTerms - slice containing the phrase terms, +// may contain empty string as placeholder (don't care) +// +// tlm - the Term Location Map containing all relevant term locations +// +// p - the current path being explored (appended to in recursive calls) +// this is the primary state being built during the traversal +// +// remainingSlop - amount of sloppiness that's allowed, which is the +// sum of the editDistances from each matching phrase part, where 0 means no +// sloppiness allowed (all editDistances must be 0), decremented during recursion +// +// rv - the final result being appended to by all the recursive calls +// +// returns slice of paths, or nil if invocation did not find any successful paths +func findPhrasePaths(prevPos uint64, ap search.ArrayPositions, phraseTerms [][]string, + tlm search.TermLocationMap, p phrasePath, remainingSlop int, rv []phrasePath) []phrasePath { + // no more terms + if len(phraseTerms) < 1 { + // snapshot or copy the recursively built phrasePath p and + // append it to the rv, also optimizing by checking if next + // phrasePath item in the rv (which we're about to overwrite) + // is available for reuse + var pcopy phrasePath + if len(rv) < cap(rv) { + pcopy = rv[:len(rv)+1][len(rv)][:0] + } + return append(rv, append(pcopy, p...)) + } + + car := phraseTerms[0] + cdr := phraseTerms[1:] + + // empty term is treated as match (continue) + if len(car) == 0 || (len(car) == 1 && car[0] == "") { + nextPos := prevPos + 1 + if prevPos == 0 { + // if prevPos was 0, don't set it to 1 (as thats not a real abs pos) + nextPos = 0 // don't advance nextPos if prevPos was 0 + } + return findPhrasePaths(nextPos, ap, cdr, tlm, p, remainingSlop, rv) + } + + // locations for this term + for _, carTerm := range car { + locations := tlm[carTerm] + LOCATIONS_LOOP: + for _, loc := range locations { + if prevPos != 0 && !loc.ArrayPositions.Equals(ap) { + // if the array positions are wrong, can't match, try next location + continue + } + + // compute distance from previous phrase term + dist := 0 + if prevPos != 0 { + dist = editDistance(prevPos+1, loc.Pos) + } + + // if enough slop remaining, continue recursively + if prevPos == 0 || (remainingSlop-dist) >= 0 { + // skip if we've already used this term+loc already + for _, ppart := range p { + if ppart.term == carTerm && ppart.loc == loc { + continue LOCATIONS_LOOP + } + } + + // this location works, add it to the path (but not for empty term) + px := append(p, phrasePart{term: carTerm, loc: loc}) + rv = findPhrasePaths(loc.Pos, loc.ArrayPositions, cdr, tlm, px, remainingSlop-dist, rv) + } + } + } + return rv +} + +func editDistance(p1, p2 uint64) int { + dist := int(p1 - p2) + if dist < 0 { + return -dist + } + return dist +} + +func (s *PhraseSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + if !s.initialized { + err := s.initSearchers(ctx) + if err != nil { + return nil, err + } + } + if s.currMust != nil { + if s.currMust.IndexInternalID.Compare(ID) >= 0 { + return s.Next(ctx) + } + ctx.DocumentMatchPool.Put(s.currMust) + } + if s.currMust == nil { + return nil, nil + } + var err error + s.currMust, err = s.mustSearcher.Advance(ctx, ID) + if err != nil { + return nil, err + } + return s.Next(ctx) +} + +func (s *PhraseSearcher) Count() uint64 { + // for now return a worst case + return s.mustSearcher.Count() +} + +func (s *PhraseSearcher) Close() error { + if s.mustSearcher != nil { + err := s.mustSearcher.Close() + if err != nil { + return err + } + } + return nil +} + +func (s *PhraseSearcher) Min() int { + return 0 +} + +func (s *PhraseSearcher) DocumentMatchPoolSize() int { + return s.mustSearcher.DocumentMatchPoolSize() + 1 +} diff --git a/search/searcher/search_phrase_test.go b/search/searcher/search_phrase_test.go new file mode 100644 index 0000000..178919d --- /dev/null +++ b/search/searcher/search_phrase_test.go @@ -0,0 +1,818 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "reflect" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestPhraseSearch(t *testing.T) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + soptions := search.SearcherOptions{Explain: true, IncludeTermVectors: true} + phraseSearcher, err := NewPhraseSearcher(context.TODO(), twoDocIndexReader, []string{"angst", "beer"}, 0, false, "desc", 1.0, soptions) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + searcher search.Searcher + results []*search.DocumentMatch + locations map[string]map[string][]search.Location + fieldterms [][2]string + }{ + { + searcher: phraseSearcher, + results: []*search.DocumentMatch{ + { + IndexInternalID: index.IndexInternalID("2"), + Score: 1.0807601687084403, + }, + }, + locations: map[string]map[string][]search.Location{"desc": {"beer": {{Pos: 2, Start: 6, End: 10}}, "angst": {{Pos: 1, Start: 0, End: 5}}}}, + fieldterms: [][2]string{{"desc", "beer"}, {"desc", "angst"}}, + }, + } + + for testIndex, test := range tests { + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(test.searcher.DocumentMatchPoolSize(), 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + next.Complete(nil) + if i < len(test.results) { + if !next.IndexInternalID.Equals(test.results[i].IndexInternalID) { + t.Errorf("expected result %d to have id %s got %s for test %d\n", i, test.results[i].IndexInternalID, next.IndexInternalID, testIndex) + } + if next.Score != test.results[i].Score { + t.Errorf("expected result %d to have score %v got %v for test %d\n", i, test.results[i].Score, next.Score, testIndex) + t.Logf("scoring explanation: %s\n", next.Expl) + } + for _, ft := range test.fieldterms { + locs := next.Locations[ft[0]][ft[1]] + explocs := test.locations[ft[0]][ft[1]] + if len(explocs) != len(locs) { + t.Fatalf("expected result %d to have %d Locations (%#v) but got %d (%#v) for test %d with field %q and term %q\n", i, len(explocs), explocs, len(locs), locs, testIndex, ft[0], ft[1]) + } + for ind, exploc := range explocs { + if !reflect.DeepEqual(*locs[ind], exploc) { + t.Errorf("expected result %d to have Location %v got %v for test %d\n", i, exploc, locs[ind], testIndex) + } + } + } + } + + ctx.DocumentMatchPool.Put(next) + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.results) != i { + t.Errorf("expected %d results got %d for test %d", len(test.results), i, testIndex) + } + } +} + +func TestMultiPhraseSearch(t *testing.T) { + soptions := search.SearcherOptions{Explain: true, IncludeTermVectors: true} + + tests := []struct { + phrase [][]string + docids [][]byte + }{ + { + phrase: [][]string{{"angst", "what"}, {"beer"}}, + docids: [][]byte{[]byte("2")}, + }, + } + + for i, test := range tests { + + reader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + searcher, err := NewMultiPhraseSearcher(context.TODO(), reader, test.phrase, 0, false, "desc", 1.0, soptions) + if err != nil { + t.Error(err) + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(searcher.DocumentMatchPoolSize(), 0), + } + next, err := searcher.Next(ctx) + var actualIds [][]byte + for err == nil && next != nil { + actualIds = append(actualIds, next.IndexInternalID) + ctx.DocumentMatchPool.Put(next) + next, err = searcher.Next(ctx) + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, i) + } + if !reflect.DeepEqual(test.docids, actualIds) { + t.Fatalf("expected ids: %v, got %v", test.docids, actualIds) + } + + err = searcher.Close() + if err != nil { + t.Error(err) + } + + err = reader.Close() + if err != nil { + t.Error(err) + } + } +} + +func TestFuzzyMultiPhraseSearch(t *testing.T) { + soptions := search.SearcherOptions{Explain: true, IncludeTermVectors: true} + + tests := []struct { + mphrase [][]string + docids [][]byte + fuzziness int + prefix int + }{ + { + mphrase: [][]string{{"pale", "anger"}, {"best"}, {"colon", "porch"}}, + docids: [][]byte{[]byte("2"), []byte("3")}, + fuzziness: 2, + }, + { + mphrase: [][]string{{"pale", "anger"}, {}, {"colon", "porch", "could"}}, + docids: nil, + fuzziness: 1, + }, + { + mphrase: [][]string{{"app"}, {"best"}, {"volume"}}, + docids: [][]byte{[]byte("3")}, + fuzziness: 2, + }, + { + mphrase: [][]string{{"anger", "pale", "bar"}, {"beard"}, {}, {}}, + docids: [][]byte{[]byte("1"), []byte("2"), []byte("3"), []byte("4")}, + fuzziness: 2, + }, + { + mphrase: [][]string{{"anger", "pale", "bar"}, {}, {"beard"}, {}}, + docids: [][]byte{[]byte("1"), []byte("4")}, + fuzziness: 2, + }, + } + + for i, test := range tests { + + reader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + searcher, err := NewMultiPhraseSearcher(context.TODO(), reader, test.mphrase, test.fuzziness, false, "desc", 1.0, soptions) + if err != nil { + t.Error(err) + } + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(searcher.DocumentMatchPoolSize(), 0), + } + next, err := searcher.Next(ctx) + var actualIds [][]byte + for err == nil && next != nil { + actualIds = append(actualIds, next.IndexInternalID) + ctx.DocumentMatchPool.Put(next) + next, err = searcher.Next(ctx) + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, i) + } + if !reflect.DeepEqual(test.docids, actualIds) { + t.Fatalf("expected ids: %v, got %v", test.docids, actualIds) + } + + err = searcher.Close() + if err != nil { + t.Error(err) + } + + err = reader.Close() + if err != nil { + t.Error(err) + } + } +} + +func TestFindPhrasePaths(t *testing.T) { + tests := []struct { + phrase [][]string + tlm search.TermLocationMap + paths []phrasePath + }{ + // simplest matching case + { + phrase: [][]string{{"cat"}, {"dog"}}, + tlm: search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 1, + }, + }, + "dog": search.Locations{ + &search.Location{ + Pos: 2, + }, + }, + }, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 2}}, + }, + }, + }, + // second term missing, no match + { + phrase: [][]string{{"cat"}, {"dog"}}, + tlm: search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 1, + }, + }, + }, + paths: nil, + }, + // second term exists but in wrong position + { + phrase: [][]string{{"cat"}, {"dog"}}, + tlm: search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 1, + }, + }, + "dog": search.Locations{ + &search.Location{ + Pos: 3, + }, + }, + }, + paths: nil, + }, + // matches multiple times + { + phrase: [][]string{{"cat"}, {"dog"}}, + tlm: search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 1, + }, + &search.Location{ + Pos: 8, + }, + }, + "dog": search.Locations{ + &search.Location{ + Pos: 2, + }, + &search.Location{ + Pos: 9, + }, + }, + }, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 2}}, + }, + { + phrasePart{"cat", &search.Location{Pos: 8}}, + phrasePart{"dog", &search.Location{Pos: 9}}, + }, + }, + }, + // match over gaps + { + phrase: [][]string{{"cat"}, {""}, {"dog"}}, + tlm: search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 1, + }, + }, + "dog": search.Locations{ + &search.Location{ + Pos: 3, + }, + }, + }, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 3}}, + }, + }, + }, + // match with leading "" + { + phrase: [][]string{{""}, {"cat"}, {"dog"}}, + tlm: search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 2, + }, + }, + "dog": search.Locations{ + &search.Location{ + Pos: 3, + }, + }, + }, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 2}}, + phrasePart{"dog", &search.Location{Pos: 3}}, + }, + }, + }, + // match with trailing "" + { + phrase: [][]string{{"cat"}, {"dog"}, {""}}, + tlm: search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 2, + }, + }, + "dog": search.Locations{ + &search.Location{ + Pos: 3, + }, + }, + }, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 2}}, + phrasePart{"dog", &search.Location{Pos: 3}}, + }, + }, + }, + } + + for i, test := range tests { + actualPaths := findPhrasePaths(0, nil, test.phrase, test.tlm, nil, 0, nil) + if !reflect.DeepEqual(actualPaths, test.paths) { + t.Fatalf("expected: %v got %v for test %d", test.paths, actualPaths, i) + } + } +} + +func TestFindPhrasePathsSloppy(t *testing.T) { + tlm := search.TermLocationMap{ + "one": search.Locations{ + &search.Location{ + Pos: 1, + }, + }, + "two": search.Locations{ + &search.Location{ + Pos: 2, + }, + }, + "three": search.Locations{ + &search.Location{ + Pos: 3, + }, + }, + "four": search.Locations{ + &search.Location{ + Pos: 4, + }, + }, + "five": search.Locations{ + &search.Location{ + Pos: 5, + }, + }, + } + + tests := []struct { + phrase [][]string + paths []phrasePath + slop int + tlm search.TermLocationMap + }{ + // no match + { + phrase: [][]string{{"one"}, {"five"}}, + slop: 2, + }, + // should match + { + phrase: [][]string{{"one"}, {"five"}}, + slop: 3, + paths: []phrasePath{ + { + phrasePart{"one", &search.Location{Pos: 1}}, + phrasePart{"five", &search.Location{Pos: 5}}, + }, + }, + }, + // slop 0 finds exact match + { + phrase: [][]string{{"four"}, {"five"}}, + slop: 0, + paths: []phrasePath{ + { + phrasePart{"four", &search.Location{Pos: 4}}, + phrasePart{"five", &search.Location{Pos: 5}}, + }, + }, + }, + // slop 0 does not find exact match (reversed) + { + phrase: [][]string{{"two"}, {"one"}}, + slop: 0, + }, + // slop 1 finds exact match + { + phrase: [][]string{{"one"}, {"two"}}, + slop: 1, + paths: []phrasePath{ + { + phrasePart{"one", &search.Location{Pos: 1}}, + phrasePart{"two", &search.Location{Pos: 2}}, + }, + }, + }, + // slop 1 *still* does not find exact match (reversed) requires at least 2 + { + phrase: [][]string{{"two"}, {"one"}}, + slop: 1, + }, + // slop 2 does finds exact match reversed + { + phrase: [][]string{{"two"}, {"one"}}, + slop: 2, + paths: []phrasePath{ + { + phrasePart{"two", &search.Location{Pos: 2}}, + phrasePart{"one", &search.Location{Pos: 1}}, + }, + }, + }, + // slop 2 not enough for this + { + phrase: [][]string{{"three"}, {"one"}}, + slop: 2, + }, + // slop should be cumulative + { + phrase: [][]string{{"one"}, {"three"}, {"five"}}, + slop: 2, + paths: []phrasePath{ + { + phrasePart{"one", &search.Location{Pos: 1}}, + phrasePart{"three", &search.Location{Pos: 3}}, + phrasePart{"five", &search.Location{Pos: 5}}, + }, + }, + }, + // should require 6 + { + phrase: [][]string{{"five"}, {"three"}, {"one"}}, + slop: 5, + }, + // so lets try 6 + { + phrase: [][]string{{"five"}, {"three"}, {"one"}}, + slop: 6, + paths: []phrasePath{ + { + phrasePart{"five", &search.Location{Pos: 5}}, + phrasePart{"three", &search.Location{Pos: 3}}, + phrasePart{"one", &search.Location{Pos: 1}}, + }, + }, + }, + // test an append() related edge case, where append()'s + // current behavior needs to be called 3 times starting from a + // nil slice before it grows to a slice with extra capacity -- + // hence, 3 initial terms of ark, bat, cat + { + phrase: [][]string{ + {"ark"}, {"bat"}, {"cat"}, {"dog"}, + }, + slop: 1, + paths: []phrasePath{ + { + phrasePart{"ark", &search.Location{Pos: 1}}, + phrasePart{"bat", &search.Location{Pos: 2}}, + phrasePart{"cat", &search.Location{Pos: 3}}, + phrasePart{"dog", &search.Location{Pos: 4}}, + }, + { + phrasePart{"ark", &search.Location{Pos: 1}}, + phrasePart{"bat", &search.Location{Pos: 2}}, + phrasePart{"cat", &search.Location{Pos: 3}}, + phrasePart{"dog", &search.Location{Pos: 5}}, + }, + }, + tlm: search.TermLocationMap{ // ark bat cat dog dog + "ark": search.Locations{ + &search.Location{Pos: 1}, + }, + "bat": search.Locations{ + &search.Location{Pos: 2}, + }, + "cat": search.Locations{ + &search.Location{Pos: 3}, + }, + "dog": search.Locations{ + &search.Location{Pos: 4}, + &search.Location{Pos: 5}, + }, + }, + }, + // test that we don't see multiple hits from the same location + { + phrase: [][]string{ + {"cat"}, {"dog"}, {"dog"}, + }, + slop: 1, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 2}}, + phrasePart{"dog", &search.Location{Pos: 3}}, + }, + }, + tlm: search.TermLocationMap{ // cat dog dog + "cat": search.Locations{ + &search.Location{Pos: 1}, + }, + "dog": search.Locations{ + &search.Location{Pos: 2}, + &search.Location{Pos: 3}, + }, + }, + }, + // test that we don't see multiple hits from the same location + { + phrase: [][]string{ + {"cat"}, {"dog"}, + }, + slop: 10, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 2}}, + }, + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 4}}, + }, + { + phrasePart{"cat", &search.Location{Pos: 3}}, + phrasePart{"dog", &search.Location{Pos: 2}}, + }, + { + phrasePart{"cat", &search.Location{Pos: 3}}, + phrasePart{"dog", &search.Location{Pos: 4}}, + }, + }, + tlm: search.TermLocationMap{ // cat dog cat dog + "cat": search.Locations{ + &search.Location{Pos: 1}, + &search.Location{Pos: 3}, + }, + "dog": search.Locations{ + &search.Location{Pos: 2}, + &search.Location{Pos: 4}, + }, + }, + }, + } + + for i, test := range tests { + tlmToUse := test.tlm + if tlmToUse == nil { + tlmToUse = tlm + } + actualPaths := findPhrasePaths(0, nil, test.phrase, tlmToUse, nil, test.slop, nil) + if !reflect.DeepEqual(actualPaths, test.paths) { + t.Fatalf("expected: %v got %v for test %d", test.paths, actualPaths, i) + } + } +} + +func TestFindPhrasePathsSloppyPalyndrome(t *testing.T) { + tlm := search.TermLocationMap{ + "one": search.Locations{ + &search.Location{ + Pos: 1, + }, + &search.Location{ + Pos: 5, + }, + }, + "two": search.Locations{ + &search.Location{ + Pos: 2, + }, + &search.Location{ + Pos: 4, + }, + }, + "three": search.Locations{ + &search.Location{ + Pos: 3, + }, + }, + } + + tests := []struct { + phrase [][]string + paths []phrasePath + slop int + }{ + // search non palyndrone, exact match + { + phrase: [][]string{{"two"}, {"three"}}, + slop: 0, + paths: []phrasePath{ + { + phrasePart{"two", &search.Location{Pos: 2}}, + phrasePart{"three", &search.Location{Pos: 3}}, + }, + }, + }, + // same with slop 2 (not required) (find it twice) + { + phrase: [][]string{{"two"}, {"three"}}, + slop: 2, + paths: []phrasePath{ + { + phrasePart{"two", &search.Location{Pos: 2}}, + phrasePart{"three", &search.Location{Pos: 3}}, + }, + { + phrasePart{"two", &search.Location{Pos: 4}}, + phrasePart{"three", &search.Location{Pos: 3}}, + }, + }, + }, + // palyndrone reversed + { + phrase: [][]string{{"three"}, {"two"}}, + slop: 2, + paths: []phrasePath{ + { + phrasePart{"three", &search.Location{Pos: 3}}, + phrasePart{"two", &search.Location{Pos: 2}}, + }, + { + phrasePart{"three", &search.Location{Pos: 3}}, + phrasePart{"two", &search.Location{Pos: 4}}, + }, + }, + }, + } + + for i, test := range tests { + actualPaths := findPhrasePaths(0, nil, test.phrase, tlm, nil, test.slop, nil) + if !reflect.DeepEqual(actualPaths, test.paths) { + t.Fatalf("expected: %v got %v for test %d", test.paths, actualPaths, i) + } + } +} + +func TestFindMultiPhrasePaths(t *testing.T) { + tlm := search.TermLocationMap{ + "cat": search.Locations{ + &search.Location{ + Pos: 1, + }, + }, + "dog": search.Locations{ + &search.Location{ + Pos: 2, + }, + }, + "frog": search.Locations{ + &search.Location{ + Pos: 3, + }, + }, + } + + tests := []struct { + phrase [][]string + paths []phrasePath + }{ + // simplest, one of two possible terms matches + { + phrase: [][]string{{"cat", "rat"}, {"dog"}}, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 2}}, + }, + }, + }, + // two possible terms, neither work + { + phrase: [][]string{{"cat", "rat"}, {"chicken"}}, + }, + // two possible terms, one works, but out of position with next + { + phrase: [][]string{{"cat", "rat"}, {"frog"}}, + }, + // matches multiple times, with different pairing + { + phrase: [][]string{{"cat", "dog"}, {"dog", "frog"}}, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"dog", &search.Location{Pos: 2}}, + }, + { + phrasePart{"dog", &search.Location{Pos: 2}}, + phrasePart{"frog", &search.Location{Pos: 3}}, + }, + }, + }, + // multi-match over a gap + { + phrase: [][]string{{"cat", "rat"}, {""}, {"frog"}}, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"frog", &search.Location{Pos: 3}}, + }, + }, + }, + // multi-match over a gap (same as before, but with empty term list) + { + phrase: [][]string{{"cat", "rat"}, {}, {"frog"}}, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"frog", &search.Location{Pos: 3}}, + }, + }, + }, + // multi-match over a gap (same once again, but nil term list) + { + phrase: [][]string{{"cat", "rat"}, nil, {"frog"}}, + paths: []phrasePath{ + { + phrasePart{"cat", &search.Location{Pos: 1}}, + phrasePart{"frog", &search.Location{Pos: 3}}, + }, + }, + }, + } + + for i, test := range tests { + actualPaths := findPhrasePaths(0, nil, test.phrase, tlm, nil, 0, nil) + if !reflect.DeepEqual(actualPaths, test.paths) { + t.Fatalf("expected: %v got %v for test %d", test.paths, actualPaths, i) + } + } +} diff --git a/search/searcher/search_regexp.go b/search/searcher/search_regexp.go new file mode 100644 index 0000000..1afdaee --- /dev/null +++ b/search/searcher/search_regexp.go @@ -0,0 +1,169 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "regexp" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +// The Regexp interface defines the subset of the regexp.Regexp API +// methods that are used by bleve indexes, allowing callers to pass in +// alternate implementations. +type Regexp interface { + FindStringIndex(s string) (loc []int) + + LiteralPrefix() (prefix string, complete bool) + + String() string +} + +// NewRegexpStringSearcher is similar to NewRegexpSearcher, but +// additionally optimizes for index readers that handle regexp's. +func NewRegexpStringSearcher(ctx context.Context, indexReader index.IndexReader, pattern string, + field string, boost float64, options search.SearcherOptions) ( + search.Searcher, error) { + ir, ok := indexReader.(index.IndexReaderRegexp) + if !ok { + r, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + + return NewRegexpSearcher(ctx, indexReader, r, field, boost, options) + } + + fieldDict, a, err := ir.FieldDictRegexpAutomaton(field, pattern) + if err != nil { + return nil, err + } + defer func() { + if cerr := fieldDict.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + var termSet = make(map[string]struct{}) + var candidateTerms []string + + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + if _, exists := termSet[tfd.Term]; !exists { + termSet[tfd.Term] = struct{}{} + candidateTerms = append(candidateTerms, tfd.Term) + tfd, err = fieldDict.Next() + } + } + if err != nil { + return nil, err + } + + if ctx != nil { + if fts, ok := ctx.Value(search.FieldTermSynonymMapKey).(search.FieldTermSynonymMap); ok { + if ts, exists := fts[field]; exists { + for term := range ts { + if _, exists := termSet[term]; exists { + continue + } + if a.MatchesRegex(term) { + termSet[term] = struct{}{} + candidateTerms = append(candidateTerms, term) + } + } + } + } + } + + return NewMultiTermSearcher(ctx, indexReader, candidateTerms, field, boost, + options, true) +} + +// NewRegexpSearcher creates a searcher which will match documents that +// contain terms which match the pattern regexp. The match must be EXACT +// matching the entire term. The provided regexp SHOULD NOT start with ^ +// or end with $ as this can intefere with the implementation. Separately, +// matches will be checked to ensure they match the entire term. +func NewRegexpSearcher(ctx context.Context, indexReader index.IndexReader, pattern Regexp, + field string, boost float64, options search.SearcherOptions) ( + search.Searcher, error) { + var candidateTerms []string + var regexpCandidates *regexpCandidates + prefixTerm, complete := pattern.LiteralPrefix() + if complete { + // there is no pattern + candidateTerms = []string{prefixTerm} + } else { + var err error + regexpCandidates, err = findRegexpCandidateTerms(indexReader, pattern, field, + prefixTerm) + if err != nil { + return nil, err + } + } + var dictBytesRead uint64 + if regexpCandidates != nil { + candidateTerms = regexpCandidates.candidates + dictBytesRead = regexpCandidates.bytesRead + } + + if ctx != nil { + reportIOStats(ctx, dictBytesRead) + search.RecordSearchCost(ctx, search.AddM, dictBytesRead) + } + + return NewMultiTermSearcher(ctx, indexReader, candidateTerms, field, boost, + options, true) +} + +type regexpCandidates struct { + candidates []string + bytesRead uint64 +} + +func findRegexpCandidateTerms(indexReader index.IndexReader, + pattern Regexp, field, prefixTerm string) (rv *regexpCandidates, err error) { + rv = ®expCandidates{ + candidates: make([]string, 0), + } + var fieldDict index.FieldDict + if len(prefixTerm) > 0 { + fieldDict, err = indexReader.FieldDictPrefix(field, []byte(prefixTerm)) + } else { + fieldDict, err = indexReader.FieldDict(field) + } + defer func() { + if cerr := fieldDict.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + // enumerate the terms and check against regexp + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + matchPos := pattern.FindStringIndex(tfd.Term) + if matchPos != nil && matchPos[0] == 0 && matchPos[1] == len(tfd.Term) { + rv.candidates = append(rv.candidates, tfd.Term) + if tooManyClauses(len(rv.candidates)) { + return rv, tooManyClausesErr(field, len(rv.candidates)) + } + } + tfd, err = fieldDict.Next() + } + rv.bytesRead = fieldDict.BytesRead() + return rv, err +} diff --git a/search/searcher/search_regexp_test.go b/search/searcher/search_regexp_test.go new file mode 100644 index 0000000..02131bb --- /dev/null +++ b/search/searcher/search_regexp_test.go @@ -0,0 +1,171 @@ +// Copyright (c) 2015 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "encoding/binary" + "fmt" + "os" + "regexp" + "testing" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestRegexpSearchUpsideDown(t *testing.T) { + twoDocIndex := initTwoDocUpsideDown() + testRegexpSearch(t, twoDocIndex, internalIDMakerUpsideDown, searcherMaker) + _ = twoDocIndex.Close() +} + +func TestRegexpStringSearchUpsideDown(t *testing.T) { + twoDocIndex := initTwoDocUpsideDown() + testRegexpSearch(t, twoDocIndex, internalIDMakerUpsideDown, searcherStringMaker) + _ = twoDocIndex.Close() +} + +func TestRegexpSearchScorch(t *testing.T) { + dir, _ := os.MkdirTemp("", "scorchTwoDoc") + defer func() { + _ = os.RemoveAll(dir) + }() + + twoDocIndex := initTwoDocScorch(dir) + testRegexpSearch(t, twoDocIndex, internalIDMakerScorch, searcherMaker) + _ = twoDocIndex.Close() +} + +func TestRegexpStringSearchScorch(t *testing.T) { + dir, _ := os.MkdirTemp("", "scorchTwoDoc") + defer func() { + _ = os.RemoveAll(dir) + }() + + twoDocIndex := initTwoDocScorch(dir) + testRegexpSearch(t, twoDocIndex, internalIDMakerScorch, searcherStringMaker) + _ = twoDocIndex.Close() +} + +func internalIDMakerUpsideDown(id int) index.IndexInternalID { + return index.IndexInternalID(fmt.Sprintf("%d", id)) +} + +func internalIDMakerScorch(id int) index.IndexInternalID { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(id)) + return index.IndexInternalID(buf) +} + +func searcherMaker(t *testing.T, ir index.IndexReader, re, field string) search.Searcher { + pattern, err := regexp.Compile(re) + if err != nil { + t.Fatal(err) + } + + regexpSearcher, err := NewRegexpSearcher(context.TODO(), ir, pattern, field, 1.0, + search.SearcherOptions{Explain: true}) + if err != nil { + t.Fatal(err) + } + + return regexpSearcher +} + +func searcherStringMaker(t *testing.T, ir index.IndexReader, re, field string) search.Searcher { + regexpSearcher, err := NewRegexpStringSearcher(context.TODO(), ir, re, field, 1.0, + search.SearcherOptions{Explain: true}) + if err != nil { + t.Fatal(err) + } + + return regexpSearcher +} + +func testRegexpSearch(t *testing.T, twoDocIndex index.Index, + internalIDMaker func(int) index.IndexInternalID, + searcherMaker func(t *testing.T, ir index.IndexReader, re, field string) search.Searcher, +) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + regexpSearcher := searcherMaker(t, twoDocIndexReader, "ma.*", "name") + regexpSearcherCo := searcherMaker(t, twoDocIndexReader, "co.*", "desc") + + tests := []struct { + searcher search.Searcher + id2score map[string]float64 + }{ + { + searcher: regexpSearcher, + id2score: map[string]float64{ + "1": 1.916290731874155, + }, + }, + { + searcher: regexpSearcherCo, + id2score: map[string]float64{ + "2": 0.33875554280828685, + "3": 0.33875554280828685, + }, + }, + } + + for testIndex, test := range tests { + defer func() { + err := test.searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(test.searcher.DocumentMatchPoolSize(), 0), + } + next, err := test.searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + exID, _ := twoDocIndexReader.ExternalID(next.IndexInternalID) + if _, ok := test.id2score[exID]; !ok { + t.Errorf("test %d, found unexpected docID = %v, next = %v", testIndex, exID, next) + } else { + score := test.id2score[exID] + if next.Score != score { + t.Errorf("test %d, expected result %d to have score %v got %v,next: %#v", + testIndex, i, score, next.Score, next) + t.Logf("scoring explanation: %s", next.Expl) + } + } + ctx.DocumentMatchPool.Put(next) + next, err = test.searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v for test %d", err, testIndex) + } + if len(test.id2score) != i { + t.Errorf("expected %d results got %d for test %d", len(test.id2score), i, testIndex) + } + } +} diff --git a/search/searcher/search_term.go b/search/searcher/search_term.go new file mode 100644 index 0000000..e11172b --- /dev/null +++ b/search/searcher/search_term.go @@ -0,0 +1,282 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "fmt" + "math" + "reflect" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/scorer" + "github.com/blevesearch/bleve/v2/size" + index "github.com/blevesearch/bleve_index_api" +) + +var reflectStaticSizeTermSearcher int + +func init() { + var ts TermSearcher + reflectStaticSizeTermSearcher = int(reflect.TypeOf(ts).Size()) +} + +type TermSearcher struct { + indexReader index.IndexReader + reader index.TermFieldReader + scorer *scorer.TermQueryScorer + tfd index.TermFieldDoc +} + +func NewTermSearcher(ctx context.Context, indexReader index.IndexReader, + term string, field string, boost float64, options search.SearcherOptions) (search.Searcher, error) { + if isTermQuery(ctx) { + ctx = context.WithValue(ctx, search.QueryTypeKey, search.Term) + } + return NewTermSearcherBytes(ctx, indexReader, []byte(term), field, boost, options) +} + +func NewTermSearcherBytes(ctx context.Context, indexReader index.IndexReader, + term []byte, field string, boost float64, options search.SearcherOptions) (search.Searcher, error) { + if ctx != nil { + if fts, ok := ctx.Value(search.FieldTermSynonymMapKey).(search.FieldTermSynonymMap); ok { + if ts, exists := fts[field]; exists { + if s, found := ts[string(term)]; found { + return NewSynonymSearcher(ctx, indexReader, term, s, field, boost, options) + } + } + } + } + needFreqNorm := options.Score != "none" + reader, err := indexReader.TermFieldReader(ctx, term, field, needFreqNorm, needFreqNorm, options.IncludeTermVectors) + if err != nil { + return nil, err + } + return newTermSearcherFromReader(ctx, indexReader, reader, term, field, boost, options) +} + +func tfIDFScoreMetrics(indexReader index.IndexReader) (uint64, error) { + // default tf-idf stats + count, err := indexReader.DocCount() + if err != nil { + return 0, err + } + + if count == 0 { + return 0, nil + } + return count, nil +} + +func bm25ScoreMetrics(ctx context.Context, field string, + indexReader index.IndexReader) (uint64, float64, error) { + var count uint64 + var fieldCardinality int + var err error + + bm25Stats, ok := ctx.Value(search.BM25StatsKey).(*search.BM25Stats) + if !ok { + count, err = indexReader.DocCount() + if err != nil { + return 0, 0, err + } + if bm25Reader, ok := indexReader.(index.BM25Reader); ok { + fieldCardinality, err = bm25Reader.FieldCardinality(field) + if err != nil { + return 0, 0, err + } + } + } else { + count = uint64(bm25Stats.DocCount) + fieldCardinality, ok = bm25Stats.FieldCardinality[field] + if !ok { + return 0, 0, fmt.Errorf("field stat for bm25 not present %s", field) + } + } + + if count == 0 && fieldCardinality == 0 { + return 0, 0, nil + } + return count, math.Ceil(float64(fieldCardinality) / float64(count)), nil +} + +func newTermSearcherFromReader(ctx context.Context, indexReader index.IndexReader, + reader index.TermFieldReader, term []byte, field string, boost float64, + options search.SearcherOptions) (*TermSearcher, error) { + var count uint64 + var avgDocLength float64 + var err error + var similarityModel string + + // as a fallback case we track certain stats for tf-idf scoring + if ctx != nil { + if similarityModelCallback, ok := ctx.Value(search. + GetScoringModelCallbackKey).(search.GetScoringModelCallbackFn); ok { + similarityModel = similarityModelCallback() + } + } + switch similarityModel { + case index.BM25Scoring: + count, avgDocLength, err = bm25ScoreMetrics(ctx, field, indexReader) + if err != nil { + _ = reader.Close() + return nil, err + } + case index.TFIDFScoring: + fallthrough + default: + count, err = tfIDFScoreMetrics(indexReader) + if err != nil { + _ = reader.Close() + return nil, err + } + } + scorer := scorer.NewTermQueryScorer(term, field, boost, count, reader.Count(), avgDocLength, options) + return &TermSearcher{ + indexReader: indexReader, + reader: reader, + scorer: scorer, + }, nil +} + +func NewSynonymSearcher(ctx context.Context, indexReader index.IndexReader, term []byte, synonyms []string, field string, boost float64, options search.SearcherOptions) (search.Searcher, error) { + createTermSearcher := func(term []byte, boostVal float64) (search.Searcher, error) { + needFreqNorm := options.Score != "none" + reader, err := indexReader.TermFieldReader(ctx, term, field, needFreqNorm, needFreqNorm, options.IncludeTermVectors) + if err != nil { + return nil, err + } + return newTermSearcherFromReader(ctx, indexReader, reader, term, field, boostVal, options) + } + // create a searcher for the term itself + termSearcher, err := createTermSearcher(term, boost) + if err != nil { + return nil, err + } + // constituent searchers of the disjunction + qsearchers := make([]search.Searcher, 0, len(synonyms)+1) + // helper method to close all the searchers we've created + // in case of an error + qsearchersClose := func() { + for _, searcher := range qsearchers { + if searcher != nil { + _ = searcher.Close() + } + } + } + qsearchers = append(qsearchers, termSearcher) + // create a searcher for each synonym + for _, synonym := range synonyms { + synonymSearcher, err := createTermSearcher([]byte(synonym), boost/2.0) + if err != nil { + qsearchersClose() + return nil, err + } + qsearchers = append(qsearchers, synonymSearcher) + } + // create a disjunction searcher + rv, err := NewDisjunctionSearcher(ctx, indexReader, qsearchers, 0, options) + if err != nil { + qsearchersClose() + return nil, err + } + return rv, nil +} + +func (s *TermSearcher) Size() int { + return reflectStaticSizeTermSearcher + size.SizeOfPtr + + s.reader.Size() + + s.tfd.Size() + + s.scorer.Size() +} + +func (s *TermSearcher) Count() uint64 { + return s.reader.Count() +} + +func (s *TermSearcher) Weight() float64 { + return s.scorer.Weight() +} + +func (s *TermSearcher) SetQueryNorm(qnorm float64) { + s.scorer.SetQueryNorm(qnorm) +} + +func (s *TermSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { + termMatch, err := s.reader.Next(s.tfd.Reset()) + if err != nil { + return nil, err + } + + if termMatch == nil { + return nil, nil + } + + // score match + docMatch := s.scorer.Score(ctx, termMatch) + // return doc match + return docMatch, nil + +} + +func (s *TermSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { + termMatch, err := s.reader.Advance(ID, s.tfd.Reset()) + if err != nil { + return nil, err + } + + if termMatch == nil { + return nil, nil + } + + // score match + docMatch := s.scorer.Score(ctx, termMatch) + + // return doc match + return docMatch, nil +} + +func (s *TermSearcher) Close() error { + return s.reader.Close() +} + +func (s *TermSearcher) Min() int { + return 0 +} + +func (s *TermSearcher) DocumentMatchPoolSize() int { + return 1 +} + +func (s *TermSearcher) Optimize(kind string, octx index.OptimizableContext) ( + index.OptimizableContext, error) { + o, ok := s.reader.(index.Optimizable) + if ok { + return o.Optimize(kind, octx) + } + + return nil, nil +} + +func isTermQuery(ctx context.Context) bool { + if ctx != nil { + // if the ctx already has a value set for query type + // it would've been done at a non term searcher level. + _, ok := ctx.Value(search.QueryTypeKey).(string) + return !ok + } + // if the context is nil, then don't set the query type + return false +} diff --git a/search/searcher/search_term_prefix.go b/search/searcher/search_term_prefix.go new file mode 100644 index 0000000..3d98cd2 --- /dev/null +++ b/search/searcher/search_term_prefix.go @@ -0,0 +1,86 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "strings" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func NewTermPrefixSearcher(ctx context.Context, indexReader index.IndexReader, prefix string, + field string, boost float64, options search.SearcherOptions) ( + search.Searcher, error) { + // find the terms with this prefix + fieldDict, err := indexReader.FieldDictPrefix(field, []byte(prefix)) + if err != nil { + return nil, err + } + defer func() { + if cerr := fieldDict.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + var terms []string + var termSet = make(map[string]struct{}) + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + if _, exists := termSet[tfd.Term]; !exists { + termSet[tfd.Term] = struct{}{} + terms = append(terms, tfd.Term) + if tooManyClauses(len(terms)) { + return nil, tooManyClausesErr(field, len(terms)) + } + tfd, err = fieldDict.Next() + } + } + if err != nil { + return nil, err + } + + if ctx != nil { + reportIOStats(ctx, fieldDict.BytesRead()) + search.RecordSearchCost(ctx, search.AddM, fieldDict.BytesRead()) + } + + if ctx != nil { + if fts, ok := ctx.Value(search.FieldTermSynonymMapKey).(search.FieldTermSynonymMap); ok { + if ts, exists := fts[field]; exists { + for term := range ts { + if _, exists := termSet[term]; exists { + continue + } + if strings.HasPrefix(term, prefix) { + termSet[term] = struct{}{} + terms = append(terms, term) + if tooManyClauses(len(terms)) { + return nil, tooManyClausesErr(field, len(terms)) + } + } + } + } + } + } + + // check if the terms are empty or have one term which is the prefix itself + if len(terms) == 0 || (len(terms) == 1 && terms[0] == prefix) { + return NewTermSearcher(ctx, indexReader, prefix, field, boost, options) + } + + return NewMultiTermSearcher(ctx, indexReader, terms, field, boost, options, true) +} diff --git a/search/searcher/search_term_range.go b/search/searcher/search_term_range.go new file mode 100644 index 0000000..990c738 --- /dev/null +++ b/search/searcher/search_term_range.go @@ -0,0 +1,92 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func NewTermRangeSearcher(ctx context.Context, indexReader index.IndexReader, + min, max []byte, inclusiveMin, inclusiveMax *bool, field string, + boost float64, options search.SearcherOptions) (search.Searcher, error) { + + if inclusiveMin == nil { + defaultInclusiveMin := true + inclusiveMin = &defaultInclusiveMin + } + if inclusiveMax == nil { + defaultInclusiveMax := false + inclusiveMax = &defaultInclusiveMax + } + + if min == nil { + min = []byte{} + } + + rangeMax := max + if rangeMax != nil { + // the term dictionary range end has an unfortunate implementation + rangeMax = append(rangeMax, 0) + } + + // find the terms with this prefix + fieldDict, err := indexReader.FieldDictRange(field, min, rangeMax) + if err != nil { + return nil, err + } + + defer func() { + if cerr := fieldDict.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + var terms []string + tfd, err := fieldDict.Next() + for err == nil && tfd != nil { + terms = append(terms, tfd.Term) + tfd, err = fieldDict.Next() + } + if err != nil { + return nil, err + } + + if len(terms) < 1 { + return NewMatchNoneSearcher(indexReader) + } + + if !*inclusiveMin && min != nil && string(min) == terms[0] { + terms = terms[1:] + // check again, as we might have removed only entry + if len(terms) < 1 { + return NewMatchNoneSearcher(indexReader) + } + } + + // if our term list included the max, it would be the last item + if !*inclusiveMax && max != nil && string(max) == terms[len(terms)-1] { + terms = terms[:len(terms)-1] + } + + if ctx != nil { + reportIOStats(ctx, fieldDict.BytesRead()) + search.RecordSearchCost(ctx, search.AddM, fieldDict.BytesRead()) + } + + return NewMultiTermSearcher(ctx, indexReader, terms, field, boost, options, true) +} diff --git a/search/searcher/search_term_range_test.go b/search/searcher/search_term_range_test.go new file mode 100644 index 0000000..703e54d --- /dev/null +++ b/search/searcher/search_term_range_test.go @@ -0,0 +1,285 @@ +// Copyright (c) 2017 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "os" + "reflect" + "sort" + "testing" + + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/search" +) + +func TestTermRangeSearch(t *testing.T) { + twoDocIndexReader, err := twoDocIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := twoDocIndexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + tests := []struct { + min []byte + max []byte + inclusiveMin bool + inclusiveMax bool + field string + want []string + }{ + { + min: []byte("marty"), + max: []byte("marty"), + field: "name", + inclusiveMin: true, + inclusiveMax: true, + want: []string{"1"}, + }, + { + min: []byte("marty"), + max: []byte("ravi"), + field: "name", + inclusiveMin: true, + inclusiveMax: true, + want: []string{"1", "4"}, + }, + // inclusive max false should exclude ravi + { + min: []byte("marty"), + max: []byte("ravi"), + field: "name", + inclusiveMin: true, + inclusiveMax: false, + want: []string{"1"}, + }, + // inclusive max false should remove last/only item + { + min: []byte("martz"), + max: []byte("ravi"), + field: "name", + inclusiveMin: true, + inclusiveMax: false, + want: nil, + }, + // inclusive min false should remove marty + { + min: []byte("marty"), + max: []byte("ravi"), + field: "name", + inclusiveMin: false, + inclusiveMax: true, + want: []string{"4"}, + }, + // inclusive min false should remove first/only item + { + min: []byte("marty"), + max: []byte("rav"), + field: "name", + inclusiveMin: false, + inclusiveMax: true, + want: nil, + }, + // max nil sees everything after marty + { + min: []byte("marty"), + max: nil, + field: "name", + inclusiveMin: true, + inclusiveMax: true, + want: []string{"1", "2", "4"}, + }, + // min nil sees everything before ravi + { + min: nil, + max: []byte("ravi"), + field: "name", + inclusiveMin: true, + inclusiveMax: true, + want: []string{"1", "3", "4", "5"}, + }, + // min and max nil sees everything + { + min: nil, + max: nil, + field: "name", + inclusiveMin: true, + inclusiveMax: true, + want: []string{"1", "2", "3", "4", "5"}, + }, + // min and max nil sees everything, even with inclusiveMin false + { + min: nil, + max: nil, + field: "name", + inclusiveMin: false, + inclusiveMax: true, + want: []string{"1", "2", "3", "4", "5"}, + }, + // min and max nil sees everything, even with inclusiveMax false + { + min: nil, + max: nil, + field: "name", + inclusiveMin: true, + inclusiveMax: false, + want: []string{"1", "2", "3", "4", "5"}, + }, + // min and max nil sees everything, even with both false + { + min: nil, + max: nil, + field: "name", + inclusiveMin: false, + inclusiveMax: false, + want: []string{"1", "2", "3", "4", "5"}, + }, + // min and max non-nil, but match 0 terms + { + min: []byte("martz"), + max: []byte("rav"), + field: "name", + inclusiveMin: true, + inclusiveMax: true, + want: nil, + }, + // min and max same (and term exists), both exlusive + { + min: []byte("marty"), + max: []byte("marty"), + field: "name", + inclusiveMin: false, + inclusiveMax: false, + want: nil, + }, + } + + for _, test := range tests { + + searcher, err := NewTermRangeSearcher(context.TODO(), twoDocIndexReader, test.min, test.max, + &test.inclusiveMin, &test.inclusiveMax, test.field, 1.0, search.SearcherOptions{Explain: true}) + if err != nil { + t.Fatal(err) + } + + var got []string + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool( + searcher.DocumentMatchPoolSize(), 0), + } + next, err := searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + got = append(got, string(next.IndexInternalID)) + ctx.DocumentMatchPool.Put(next) + next, err = searcher.Next(ctx) + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v", err) + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("expected: %v, got %v for test %#v", test.want, got, test) + } + + } +} + +func TestTermRangeSearchTooManyTerms(t *testing.T) { + dir, _ := os.MkdirTemp("", "scorchTwoDoc") + defer func() { + _ = os.RemoveAll(dir) + }() + + scorchIndex := initTwoDocScorch(dir) + + // use lower limit for this test + origLimit := DisjunctionMaxClauseCount + DisjunctionMaxClauseCount = 2 + defer func() { + DisjunctionMaxClauseCount = origLimit + }() + + scorchReader, err := scorchIndex.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := scorchReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + want := []string{"1", "3", "4", "5"} + truth := true + searcher, err := NewTermRangeSearcher(context.TODO(), scorchReader, []byte("bobert"), []byte("ravi"), + &truth, &truth, "name", 1.0, search.SearcherOptions{Score: "none", IncludeTermVectors: false}) + if err != nil { + t.Fatal(err) + } + + var got []string + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool( + searcher.DocumentMatchPoolSize(), 0), + } + next, err := searcher.Next(ctx) + i := 0 + for err == nil && next != nil { + extId, err := scorchReader.ExternalID(next.IndexInternalID) + if err != nil { + t.Fatal(err) + } + got = append(got, extId) + ctx.DocumentMatchPool.Put(next) + next, err = searcher.Next(ctx) + if err != nil { + break + } + + i++ + } + if err != nil { + t.Fatalf("error iterating searcher: %v", err) + } + err = searcher.Close() + if err != nil { + t.Fatal(err) + } + + // check that the expected number of term searchers were started + // 6 = 4 original terms, 1 optimized after first round, then final searcher + // from the last round + statsMap := scorchIndex.(*scorch.Scorch).StatsMap() + if statsMap["term_searchers_started"].(uint64) != 6 { + t.Errorf("expected 6 term searchers started, got %d", statsMap["term_searchers_started"]) + } + // check that all started searchers were closed + if statsMap["term_searchers_started"] != statsMap["term_searchers_finished"] { + t.Errorf("expected all term searchers closed, %d started %d closed", + statsMap["term_searchers_started"], statsMap["term_searchers_finished"]) + } + + sort.Strings(got) + if !reflect.DeepEqual(got, want) { + t.Errorf("expected: %#v, got %#v", want, got) + } +} diff --git a/search/searcher/search_term_test.go b/search/searcher/search_term_test.go new file mode 100644 index 0000000..e87db9d --- /dev/null +++ b/search/searcher/search_term_test.go @@ -0,0 +1,185 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searcher + +import ( + "context" + "math" + "testing" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestTermSearcher(t *testing.T) { + queryTerm := "beer" + queryField := "desc" + queryBoost := 3.0 + queryExplain := search.SearcherOptions{Explain: true} + + analysisQueue := index.NewAnalysisQueue(1) + i, err := upsidedown.NewUpsideDownCouch( + gtreap.Name, + map[string]interface{}{ + "path": "", + }, + analysisQueue) + if err != nil { + t.Fatal(err) + } + err = i.Open() + if err != nil { + t.Fatal(err) + } + doc := document.NewDocument("a") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("b") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("c") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("d") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("e") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("f") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("g") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("h") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("i") + doc.AddField(document.NewTextField("desc", []uint64{}, []byte("beer"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + doc = document.NewDocument("j") + doc.AddField(document.NewTextField("title", []uint64{}, []byte("cat"))) + err = i.Update(doc) + if err != nil { + t.Fatal(err) + } + + indexReader, err := i.Reader() + if err != nil { + t.Error(err) + } + defer func() { + err := indexReader.Close() + if err != nil { + t.Fatal(err) + } + }() + + searcher, err := NewTermSearcher(context.TODO(), indexReader, queryTerm, queryField, queryBoost, queryExplain) + if err != nil { + t.Fatal(err) + } + defer func() { + err := searcher.Close() + if err != nil { + t.Fatal(err) + } + }() + + searcher.SetQueryNorm(2.0) + docCount, err := indexReader.DocCount() + if err != nil { + t.Fatal(err) + } + idf := 1.0 + math.Log(float64(docCount)/float64(searcher.Count()+1.0)) + expectedQueryWeight := 3 * idf * 3 * idf + if expectedQueryWeight != searcher.Weight() { + t.Errorf("expected weight %v got %v", expectedQueryWeight, searcher.Weight()) + } + + if searcher.Count() != 9 { + t.Errorf("expected count of 9, got %d", searcher.Count()) + } + + ctx := &search.SearchContext{ + DocumentMatchPool: search.NewDocumentMatchPool(1, 0), + } + docMatch, err := searcher.Next(ctx) + if err != nil { + t.Errorf("expected result, got %v", err) + } + if !docMatch.IndexInternalID.Equals(index.IndexInternalID("a")) { + t.Errorf("expected result ID to be 'a', got '%s", docMatch.IndexInternalID) + } + ctx.DocumentMatchPool.Put(docMatch) + docMatch, err = searcher.Advance(ctx, index.IndexInternalID("c")) + if err != nil { + t.Errorf("expected result, got %v", err) + } + if !docMatch.IndexInternalID.Equals(index.IndexInternalID("c")) { + t.Errorf("expected result ID to be 'c' got '%s'", docMatch.IndexInternalID) + } + + // try advancing past end + ctx.DocumentMatchPool.Put(docMatch) + docMatch, err = searcher.Advance(ctx, index.IndexInternalID("z")) + if err != nil { + t.Fatal(err) + } + if docMatch != nil { + t.Errorf("expected nil, got %v", docMatch) + } + + // try pushing next past end + ctx.DocumentMatchPool.Put(docMatch) + docMatch, err = searcher.Next(ctx) + if err != nil { + t.Fatal(err) + } + if docMatch != nil { + t.Errorf("expected nil, got %v", docMatch) + } +} diff --git a/search/sort.go b/search/sort.go new file mode 100644 index 0000000..67f143e --- /dev/null +++ b/search/sort.go @@ -0,0 +1,764 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "sort" + "strings" + "unicode/utf8" + + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/numeric" + "github.com/blevesearch/bleve/v2/util" +) + +var ( + HighTerm = strings.Repeat(string(utf8.MaxRune), 3) + LowTerm = string([]byte{0x00}) +) + +type SearchSort interface { + UpdateVisitor(field string, term []byte) + Value(a *DocumentMatch) string + Descending() bool + + RequiresDocID() bool + RequiresScoring() bool + RequiresFields() []string + + Reverse() + + Copy() SearchSort +} + +func ParseSearchSortObj(input map[string]interface{}) (SearchSort, error) { + descending, ok := input["desc"].(bool) + if !ok { + descending = false + } + + by, ok := input["by"].(string) + if !ok { + return nil, fmt.Errorf("search sort must specify by") + } + + switch by { + case "id": + return &SortDocID{ + Desc: descending, + }, nil + case "score": + return &SortScore{ + Desc: descending, + }, nil + case "geo_distance": + field, ok := input["field"].(string) + if !ok { + return nil, fmt.Errorf("search sort mode geo_distance must specify field") + } + lon, lat, foundLocation := geo.ExtractGeoPoint(input["location"]) + if !foundLocation { + return nil, fmt.Errorf("unable to parse geo_distance location") + } + rvd := &SortGeoDistance{ + Field: field, + Desc: descending, + Lon: lon, + Lat: lat, + unitMult: 1.0, + } + if distUnit, ok := input["unit"].(string); ok { + var err error + rvd.unitMult, err = geo.ParseDistanceUnit(distUnit) + if err != nil { + return nil, err + } + rvd.Unit = distUnit + } + return rvd, nil + case "field": + field, ok := input["field"].(string) + if !ok { + return nil, fmt.Errorf("search sort mode field must specify field") + } + rv := &SortField{ + Field: field, + Desc: descending, + } + typ, ok := input["type"].(string) + if ok { + switch typ { + case "auto": + rv.Type = SortFieldAuto + case "string": + rv.Type = SortFieldAsString + case "number": + rv.Type = SortFieldAsNumber + case "date": + rv.Type = SortFieldAsDate + default: + return nil, fmt.Errorf("unknown sort field type: %s", typ) + } + } + mode, ok := input["mode"].(string) + if ok { + switch mode { + case "default": + rv.Mode = SortFieldDefault + case "min": + rv.Mode = SortFieldMin + case "max": + rv.Mode = SortFieldMax + default: + return nil, fmt.Errorf("unknown sort field mode: %s", mode) + } + } + missing, ok := input["missing"].(string) + if ok { + switch missing { + case "first": + rv.Missing = SortFieldMissingFirst + case "last": + rv.Missing = SortFieldMissingLast + default: + return nil, fmt.Errorf("unknown sort field missing: %s", missing) + } + } + return rv, nil + } + + return nil, fmt.Errorf("unknown search sort by: %s", by) +} + +func ParseSearchSortString(input string) SearchSort { + descending := false + if strings.HasPrefix(input, "-") { + descending = true + input = input[1:] + } else if strings.HasPrefix(input, "+") { + input = input[1:] + } + + switch input { + case "_id": + return &SortDocID{ + Desc: descending, + } + case "_score": + return &SortScore{ + Desc: descending, + } + } + + return &SortField{ + Field: input, + Desc: descending, + } +} + +func ParseSearchSortJSON(input json.RawMessage) (SearchSort, error) { + // first try to parse it as string + var sortString string + err := util.UnmarshalJSON(input, &sortString) + if err != nil { + var sortObj map[string]interface{} + err = util.UnmarshalJSON(input, &sortObj) + if err != nil { + return nil, err + } + return ParseSearchSortObj(sortObj) + } + return ParseSearchSortString(sortString), nil +} + +func ParseSortOrderStrings(in []string) SortOrder { + rv := make(SortOrder, 0, len(in)) + for _, i := range in { + ss := ParseSearchSortString(i) + rv = append(rv, ss) + } + return rv +} + +func ParseSortOrderJSON(in []json.RawMessage) (SortOrder, error) { + rv := make(SortOrder, 0, len(in)) + for _, i := range in { + ss, err := ParseSearchSortJSON(i) + if err != nil { + return nil, err + } + rv = append(rv, ss) + } + return rv, nil +} + +type SortOrder []SearchSort + +func (so SortOrder) Value(doc *DocumentMatch) { + for _, soi := range so { + doc.Sort = append(doc.Sort, soi.Value(doc)) + } +} + +func (so SortOrder) UpdateVisitor(field string, term []byte) { + for _, soi := range so { + soi.UpdateVisitor(field, term) + } +} + +func (so SortOrder) Copy() SortOrder { + rv := make(SortOrder, len(so)) + for i, soi := range so { + rv[i] = soi.Copy() + } + return rv +} + +// Compare will compare two document matches using the specified sort order +// if both are numbers, we avoid converting back to term +func (so SortOrder) Compare(cachedScoring, cachedDesc []bool, i, j *DocumentMatch) int { + // compare the documents on all search sorts until a differences is found + for x := range so { + c := 0 + if cachedScoring[x] { + if i.Score < j.Score { + c = -1 + } else if i.Score > j.Score { + c = 1 + } + } else { + iVal := i.Sort[x] + jVal := j.Sort[x] + if iVal < jVal { + c = -1 + } else if iVal > jVal { + c = 1 + } + } + + if c == 0 { + continue + } + if cachedDesc[x] { + c = -c + } + return c + } + // if they are the same at this point, impose order based on index natural sort order + if i.HitNumber == j.HitNumber { + return 0 + } else if i.HitNumber > j.HitNumber { + return 1 + } + return -1 +} + +func (so SortOrder) RequiresScore() bool { + for _, soi := range so { + if soi.RequiresScoring() { + return true + } + } + return false +} + +func (so SortOrder) RequiresDocID() bool { + for _, soi := range so { + if soi.RequiresDocID() { + return true + } + } + return false +} + +func (so SortOrder) RequiredFields() []string { + var rv []string + for _, soi := range so { + rv = append(rv, soi.RequiresFields()...) + } + return rv +} + +func (so SortOrder) CacheIsScore() []bool { + rv := make([]bool, 0, len(so)) + for _, soi := range so { + rv = append(rv, soi.RequiresScoring()) + } + return rv +} + +func (so SortOrder) CacheDescending() []bool { + rv := make([]bool, 0, len(so)) + for _, soi := range so { + rv = append(rv, soi.Descending()) + } + return rv +} + +func (so SortOrder) Reverse() { + for _, soi := range so { + soi.Reverse() + } +} + +// SortFieldType lets you control some internal sort behavior +// normally leaving this to the zero-value of SortFieldAuto is fine +type SortFieldType int + +const ( + // SortFieldAuto applies heuristics attempt to automatically sort correctly + SortFieldAuto SortFieldType = iota + // SortFieldAsString forces sort as string (no prefix coded terms removed) + SortFieldAsString + // SortFieldAsNumber forces sort as string (prefix coded terms with shift > 0 removed) + SortFieldAsNumber + // SortFieldAsDate forces sort as string (prefix coded terms with shift > 0 removed) + SortFieldAsDate +) + +// SortFieldMode describes the behavior if the field has multiple values +type SortFieldMode int + +const ( + // SortFieldDefault uses the first (or only) value, this is the default zero-value + SortFieldDefault SortFieldMode = iota // FIXME name is confusing + // SortFieldMin uses the minimum value + SortFieldMin + // SortFieldMax uses the maximum value + SortFieldMax +) + +// SortFieldMissing controls where documents missing a field value should be sorted +type SortFieldMissing int + +const ( + // SortFieldMissingLast sorts documents missing a field at the end + SortFieldMissingLast SortFieldMissing = iota + + // SortFieldMissingFirst sorts documents missing a field at the beginning + SortFieldMissingFirst +) + +// SortField will sort results by the value of a stored field +// +// Field is the name of the field +// Descending reverse the sort order (default false) +// Type allows forcing of string/number/date behavior (default auto) +// Mode controls behavior for multi-values fields (default first) +// Missing controls behavior of missing values (default last) +type SortField struct { + Field string + Desc bool + Type SortFieldType + Mode SortFieldMode + Missing SortFieldMissing + values [][]byte + tmp [][]byte +} + +// UpdateVisitor notifies this sort field that in this document +// this field has the specified term +func (s *SortField) UpdateVisitor(field string, term []byte) { + if field == s.Field { + s.values = append(s.values, term) + } +} + +// Value returns the sort value of the DocumentMatch +// it also resets the state of this SortField for +// processing the next document +func (s *SortField) Value(i *DocumentMatch) string { + iTerms := s.filterTermsByType(s.values) + iTerm := s.filterTermsByMode(iTerms) + s.values = s.values[:0] + return iTerm +} + +// Descending determines the order of the sort +func (s *SortField) Descending() bool { + return s.Desc +} + +func (s *SortField) filterTermsByMode(terms [][]byte) string { + if len(terms) == 1 || (len(terms) > 1 && s.Mode == SortFieldDefault) { + return string(terms[0]) + } else if len(terms) > 1 { + switch s.Mode { + case SortFieldMin: + sort.Sort(BytesSlice(terms)) + return string(terms[0]) + case SortFieldMax: + sort.Sort(BytesSlice(terms)) + return string(terms[len(terms)-1]) + } + } + + // handle missing terms + if s.Missing == SortFieldMissingLast { + if s.Desc { + return LowTerm + } + return HighTerm + } + if s.Desc { + return HighTerm + } + return LowTerm +} + +// filterTermsByType attempts to make one pass on the terms +// if we are in auto-mode AND all the terms look like prefix-coded numbers +// return only the terms which had shift of 0 +// if we are in explicit number or date mode, return only valid +// prefix coded numbers with shift of 0 +func (s *SortField) filterTermsByType(terms [][]byte) [][]byte { + stype := s.Type + + switch stype { + case SortFieldAuto: + allTermsPrefixCoded := true + termsWithShiftZero := s.tmp[:0] + for _, term := range terms { + valid, shift := numeric.ValidPrefixCodedTermBytes(term) + if valid && shift == 0 { + termsWithShiftZero = append(termsWithShiftZero, term) + } else if !valid { + allTermsPrefixCoded = false + } + } + // reset the terms only when valid zero shift terms are found. + if allTermsPrefixCoded && len(termsWithShiftZero) > 0 { + terms = termsWithShiftZero + s.tmp = termsWithShiftZero[:0] + } + case SortFieldAsNumber, SortFieldAsDate: + termsWithShiftZero := s.tmp[:0] + for _, term := range terms { + valid, shift := numeric.ValidPrefixCodedTermBytes(term) + if valid && shift == 0 { + termsWithShiftZero = append(termsWithShiftZero, term) + } + } + terms = termsWithShiftZero + s.tmp = termsWithShiftZero[:0] + } + + return terms +} + +// RequiresDocID says this SearchSort does not require the DocID be loaded +func (s *SortField) RequiresDocID() bool { return false } + +// RequiresScoring says this SearchStore does not require scoring +func (s *SortField) RequiresScoring() bool { return false } + +// RequiresFields says this SearchStore requires the specified stored field +func (s *SortField) RequiresFields() []string { return []string{s.Field} } + +func (s *SortField) MarshalJSON() ([]byte, error) { + // see if simple format can be used + if s.Missing == SortFieldMissingLast && + s.Mode == SortFieldDefault && + s.Type == SortFieldAuto { + if s.Desc { + return json.Marshal("-" + s.Field) + } + return json.Marshal(s.Field) + } + sfm := map[string]interface{}{ + "by": "field", + "field": s.Field, + } + if s.Desc { + sfm["desc"] = true + } + if s.Missing > SortFieldMissingLast { + switch s.Missing { + case SortFieldMissingFirst: + sfm["missing"] = "first" + } + } + if s.Mode > SortFieldDefault { + switch s.Mode { + case SortFieldMin: + sfm["mode"] = "min" + case SortFieldMax: + sfm["mode"] = "max" + } + } + if s.Type > SortFieldAuto { + switch s.Type { + case SortFieldAsString: + sfm["type"] = "string" + case SortFieldAsNumber: + sfm["type"] = "number" + case SortFieldAsDate: + sfm["type"] = "date" + } + } + + return json.Marshal(sfm) +} + +func (s *SortField) Copy() SearchSort { + rv := *s + return &rv +} + +func (s *SortField) Reverse() { + s.Desc = !s.Desc + if s.Missing == SortFieldMissingFirst { + s.Missing = SortFieldMissingLast + } else { + s.Missing = SortFieldMissingFirst + } +} + +// SortDocID will sort results by the document identifier +type SortDocID struct { + Desc bool +} + +// UpdateVisitor is a no-op for SortDocID as it's value +// is not dependent on any field terms +func (s *SortDocID) UpdateVisitor(field string, term []byte) { +} + +// Value returns the sort value of the DocumentMatch +func (s *SortDocID) Value(i *DocumentMatch) string { + return i.ID +} + +// Descending determines the order of the sort +func (s *SortDocID) Descending() bool { + return s.Desc +} + +// RequiresDocID says this SearchSort does require the DocID be loaded +func (s *SortDocID) RequiresDocID() bool { return true } + +// RequiresScoring says this SearchStore does not require scoring +func (s *SortDocID) RequiresScoring() bool { return false } + +// RequiresFields says this SearchStore does not require any stored fields +func (s *SortDocID) RequiresFields() []string { return nil } + +func (s *SortDocID) MarshalJSON() ([]byte, error) { + if s.Desc { + return json.Marshal("-_id") + } + return json.Marshal("_id") +} + +func (s *SortDocID) Copy() SearchSort { + rv := *s + return &rv +} + +func (s *SortDocID) Reverse() { + s.Desc = !s.Desc +} + +// SortScore will sort results by the document match score +type SortScore struct { + Desc bool +} + +// UpdateVisitor is a no-op for SortScore as it's value +// is not dependent on any field terms +func (s *SortScore) UpdateVisitor(field string, term []byte) { +} + +// Value returns the sort value of the DocumentMatch +func (s *SortScore) Value(i *DocumentMatch) string { + return "_score" +} + +// Descending determines the order of the sort +func (s *SortScore) Descending() bool { + return s.Desc +} + +// RequiresDocID says this SearchSort does not require the DocID be loaded +func (s *SortScore) RequiresDocID() bool { return false } + +// RequiresScoring says this SearchStore does require scoring +func (s *SortScore) RequiresScoring() bool { return true } + +// RequiresFields says this SearchStore does not require any store fields +func (s *SortScore) RequiresFields() []string { return nil } + +func (s *SortScore) MarshalJSON() ([]byte, error) { + if s.Desc { + return json.Marshal("-_score") + } + return json.Marshal("_score") +} + +func (s *SortScore) Copy() SearchSort { + rv := *s + return &rv +} + +func (s *SortScore) Reverse() { + s.Desc = !s.Desc +} + +var maxDistance = string(numeric.MustNewPrefixCodedInt64(math.MaxInt64, 0)) + +// NewSortGeoDistance creates SearchSort instance for sorting documents by +// their distance from the specified point. +func NewSortGeoDistance(field, unit string, lon, lat float64, desc bool) ( + *SortGeoDistance, error, +) { + rv := &SortGeoDistance{ + Field: field, + Desc: desc, + Unit: unit, + Lon: lon, + Lat: lat, + } + var err error + rv.unitMult, err = geo.ParseDistanceUnit(unit) + if err != nil { + return nil, err + } + return rv, nil +} + +// SortGeoDistance will sort results by the distance of an +// indexed geo point, from the provided location. +// +// Field is the name of the field +// Descending reverse the sort order (default false) +type SortGeoDistance struct { + Field string + Desc bool + Unit string + values []string + Lon float64 + Lat float64 + unitMult float64 +} + +// UpdateVisitor notifies this sort field that in this document +// this field has the specified term +func (s *SortGeoDistance) UpdateVisitor(field string, term []byte) { + if field == s.Field { + s.values = append(s.values, string(term)) + } +} + +// Value returns the sort value of the DocumentMatch +// it also resets the state of this SortField for +// processing the next document +func (s *SortGeoDistance) Value(i *DocumentMatch) string { + iTerms := s.filterTermsByType(s.values) + iTerm := s.filterTermsByMode(iTerms) + s.values = s.values[:0] + + if iTerm == "" { + return maxDistance + } + + i64, err := numeric.PrefixCoded(iTerm).Int64() + if err != nil { + return maxDistance + } + docLon := geo.MortonUnhashLon(uint64(i64)) + docLat := geo.MortonUnhashLat(uint64(i64)) + + dist := geo.Haversin(s.Lon, s.Lat, docLon, docLat) + // dist is returned in km, so convert to m + dist *= 1000 + if s.unitMult != 0 { + dist /= s.unitMult + } + distInt64 := numeric.Float64ToInt64(dist) + return string(numeric.MustNewPrefixCodedInt64(distInt64, 0)) +} + +// Descending determines the order of the sort +func (s *SortGeoDistance) Descending() bool { + return s.Desc +} + +func (s *SortGeoDistance) filterTermsByMode(terms []string) string { + if len(terms) >= 1 { + return terms[0] + } + + return "" +} + +// filterTermsByType attempts to make one pass on the terms +// return only valid prefix coded numbers with shift of 0 +func (s *SortGeoDistance) filterTermsByType(terms []string) []string { + var termsWithShiftZero []string + for _, term := range terms { + valid, shift := numeric.ValidPrefixCodedTerm(term) + if valid && shift == 0 { + termsWithShiftZero = append(termsWithShiftZero, term) + } + } + return termsWithShiftZero +} + +// RequiresDocID says this SearchSort does not require the DocID be loaded +func (s *SortGeoDistance) RequiresDocID() bool { return false } + +// RequiresScoring says this SearchStore does not require scoring +func (s *SortGeoDistance) RequiresScoring() bool { return false } + +// RequiresFields says this SearchStore requires the specified stored field +func (s *SortGeoDistance) RequiresFields() []string { return []string{s.Field} } + +func (s *SortGeoDistance) MarshalJSON() ([]byte, error) { + sfm := map[string]interface{}{ + "by": "geo_distance", + "field": s.Field, + "location": map[string]interface{}{ + "lon": s.Lon, + "lat": s.Lat, + }, + } + if s.Unit != "" { + sfm["unit"] = s.Unit + } + if s.Desc { + sfm["desc"] = true + } + + return json.Marshal(sfm) +} + +func (s *SortGeoDistance) Copy() SearchSort { + rv := *s + return &rv +} + +func (s *SortGeoDistance) Reverse() { + s.Desc = !s.Desc +} + +type BytesSlice [][]byte + +func (p BytesSlice) Len() int { return len(p) } +func (p BytesSlice) Less(i, j int) bool { return bytes.Compare(p[i], p[j]) < 0 } +func (p BytesSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/search/sort_test.go b/search/sort_test.go new file mode 100644 index 0000000..009444b --- /dev/null +++ b/search/sort_test.go @@ -0,0 +1,338 @@ +package search + +import ( + "reflect" + "testing" +) + +func TestParseSearchSortObj(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + want SearchSort + wantErr bool + }{ + { + name: "sort by id", + input: map[string]interface{}{ + "by": "id", + "desc": false, + }, + want: &SortDocID{ + Desc: false, + }, + wantErr: false, + }, + { + name: "sort by id descending", + input: map[string]interface{}{ + "by": "id", + "desc": true, + }, + want: &SortDocID{ + Desc: true, + }, + wantErr: false, + }, + { + name: "sort by score", + input: map[string]interface{}{ + "by": "score", + "desc": false, + }, + want: &SortScore{ + Desc: false, + }, + wantErr: false, + }, + { + name: "sort by score descending", + input: map[string]interface{}{ + "by": "score", + "desc": true, + }, + want: &SortScore{ + Desc: true, + }, + wantErr: false, + }, + { + name: "sort by geo_distance", + input: map[string]interface{}{ + "by": "geo_distance", + "field": "location", + "location": map[string]interface{}{ + "lon": 1.0, + "lat": 2.0, + }, + "unit": "km", + "desc": false, + }, + want: &SortGeoDistance{ + Field: "location", + Desc: false, + Lon: 1.0, + Lat: 2.0, + Unit: "km", + unitMult: 1000.0, + }, + wantErr: false, + }, + { + name: "sort by field", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "desc": false, + "type": "auto", + "mode": "default", + "missing": "last", + }, + want: &SortField{ + Field: "name", + Desc: false, + Type: SortFieldAuto, + Mode: SortFieldDefault, + Missing: SortFieldMissingLast, + }, + wantErr: false, + }, + { + name: "sort by field with missing", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "desc": false, + "type": "auto", + "mode": "default", + "missing": "first", + }, + want: &SortField{ + Field: "name", + Desc: false, + Type: SortFieldAuto, + Mode: SortFieldDefault, + Missing: SortFieldMissingFirst, + }, + wantErr: false, + }, + { + name: "sort by field descending", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "desc": true, + "type": "string", + "mode": "min", + "missing": "first", + }, + want: &SortField{ + Field: "name", + Desc: true, + Type: SortFieldAsString, + Mode: SortFieldMin, + Missing: SortFieldMissingFirst, + }, + wantErr: false, + }, + { + name: "missing by", + input: map[string]interface{}{ + "desc": true, + }, + want: nil, + wantErr: true, + }, + { + name: "unknown by", + input: map[string]interface{}{ + "by": "unknown", + }, + want: nil, + wantErr: true, + }, + { + name: "missing field for geo_distance", + input: map[string]interface{}{ + "by": "geo_distance", + "location": map[string]interface{}{ + "lon": 1.0, + "lat": 2.0, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "missing location for geo_distance", + input: map[string]interface{}{ + "by": "geo_distance", + "field": "location", + }, + want: nil, + wantErr: true, + }, + { + name: "invalid unit for geo_distance", + input: map[string]interface{}{ + "by": "geo_distance", + "field": "location", + "location": map[string]interface{}{ + "lon": 1.0, + "lat": 2.0, + }, + "unit": "invalid", + }, + want: nil, + wantErr: true, + }, + { + name: "missing field for field sort", + input: map[string]interface{}{ + "by": "field", + }, + want: nil, + wantErr: true, + }, + { + name: "unknown type for field sort", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "type": "unknown", + }, + want: nil, + wantErr: true, + }, + { + name: "number type for field sort with desc", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "type": "number", + "mode": "default", + "desc": true, + "missing": "last", + }, + want: &SortField{ + Field: "name", + Desc: true, + Type: SortFieldAsNumber, + Mode: SortFieldDefault, + Missing: SortFieldMissingLast, + }, + wantErr: false, + }, + { + name: "date type for field sort with desc", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "type": "date", + "mode": "default", + "desc": true, + "missing": "last", + }, + want: &SortField{ + Field: "name", + Desc: true, + Type: SortFieldAsDate, + Mode: SortFieldDefault, + Missing: SortFieldMissingLast, + }, + wantErr: false, + }, + { + name: "unknown type for field sort with missing", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "type": "unknown", + "mode": "default", + "missing": "last", + }, + want: nil, + wantErr: true, + }, + { + name: "unknown mode for field sort", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "mode": "unknown", + }, + want: nil, + wantErr: true, + }, + { + name: "default mode for field sort", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "mode": "default", + }, + want: &SortField{ + Field: "name", + Desc: false, + Type: SortFieldAuto, + Mode: SortFieldDefault, + Missing: SortFieldMissingLast, + }, + wantErr: false, + }, + { + name: "max mode for field sort", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "mode": "max", + }, + want: &SortField{ + Field: "name", + Desc: false, + Type: SortFieldAuto, + Mode: SortFieldMax, + Missing: SortFieldMissingLast, + }, + wantErr: false, + }, + { + name: "min mode for field sort", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "mode": "min", + }, + want: &SortField{ + Field: "name", + Desc: false, + Type: SortFieldAuto, + Mode: SortFieldMin, + Missing: SortFieldMissingLast, + }, + wantErr: false, + }, + { + name: "unknown missing for field sort", + input: map[string]interface{}{ + "by": "field", + "field": "name", + "missing": "unknown", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseSearchSortObj(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseSearchSortObj() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseSearchSortObj() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/search/util.go b/search/util.go new file mode 100644 index 0000000..06f8f99 --- /dev/null +++ b/search/util.go @@ -0,0 +1,235 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "context" + + "github.com/blevesearch/geo/s2" +) + +func MergeLocations(locations []FieldTermLocationMap) FieldTermLocationMap { + rv := locations[0] + + for i := 1; i < len(locations); i++ { + nextLocations := locations[i] + for field, termLocationMap := range nextLocations { + rvTermLocationMap, rvHasField := rv[field] + if rvHasField { + rv[field] = MergeTermLocationMaps(rvTermLocationMap, termLocationMap) + } else { + rv[field] = termLocationMap + } + } + } + + return rv +} + +func MergeTermLocationMaps(rv, other TermLocationMap) TermLocationMap { + for term, locationMap := range other { + // for a given term/document there cannot be different locations + // if they came back from different clauses, overwrite is ok + rv[term] = locationMap + } + return rv +} + +func MergeFieldTermLocations(dest []FieldTermLocation, matches []*DocumentMatch) []FieldTermLocation { + n := len(dest) + for _, dm := range matches { + n += len(dm.FieldTermLocations) + } + if cap(dest) < n { + dest = append(make([]FieldTermLocation, 0, n), dest...) + } + + for _, dm := range matches { + for _, ftl := range dm.FieldTermLocations { + dest = append(dest, FieldTermLocation{ + Field: ftl.Field, + Term: ftl.Term, + Location: Location{ + Pos: ftl.Location.Pos, + Start: ftl.Location.Start, + End: ftl.Location.End, + ArrayPositions: append(ArrayPositions(nil), ftl.Location.ArrayPositions...), + }, + }) + } + } + + return dest +} + +type SearchIOStatsCallbackFunc func(uint64) + +// Implementation of SearchIncrementalCostCallbackFn should handle the following messages +// - add: increment the cost of a search operation +// (which can be specific to a query type as well) +// - abort: query was aborted due to a cancel of search's context (for eg), +// which can be handled differently as well +// - done: indicates that a search was complete and the tracked cost can be +// handled safely by the implementation. +type SearchIncrementalCostCallbackFn func(SearchIncrementalCostCallbackMsg, + SearchQueryType, uint64) + +type ( + SearchIncrementalCostCallbackMsg uint + SearchQueryType uint +) + +const ( + Term = SearchQueryType(1 << iota) + Geo + Numeric + GenericCost +) + +const ( + AddM = SearchIncrementalCostCallbackMsg(1 << iota) + AbortM + DoneM +) + +// ContextKey is used to identify the context key in the context.Context +type ContextKey string + +func (c ContextKey) String() string { + return string(c) +} + +const ( + SearchIncrementalCostKey ContextKey = "_search_incremental_cost_key" + QueryTypeKey ContextKey = "_query_type_key" + FuzzyMatchPhraseKey ContextKey = "_fuzzy_match_phrase_key" + IncludeScoreBreakdownKey ContextKey = "_include_score_breakdown_key" + + // PreSearchKey indicates whether to perform a preliminary search to gather necessary + // information which would be used in the actual search down the line. + PreSearchKey ContextKey = "_presearch_key" + + // GetScoringModelCallbackKey is used to help the underlying searcher identify + // which scoring mechanism to use based on index mapping. + GetScoringModelCallbackKey ContextKey = "_get_scoring_model" + + // SearchIOStatsCallbackKey is used to help the underlying searcher identify + SearchIOStatsCallbackKey ContextKey = "_search_io_stats_callback_key" + + // GeoBufferPoolCallbackKey ContextKey is used to help the underlying searcher + GeoBufferPoolCallbackKey ContextKey = "_geo_buffer_pool_callback_key" + + // SearchTypeKey is used to identify type of the search being performed. + // + // for consistent scoring in cases an index is partitioned/sharded (using an + // index alias), GlobalScoring helps in aggregating the necessary stats across + // all the child bleve indexes (shards/partitions) first before the actual search + // is performed, such that the scoring involved using these stats would be at a + // global level. + SearchTypeKey ContextKey = "_search_type_key" + + // The following keys are used to invoke the callbacks at the start and end stages + // of optimizing the disjunction/conjunction searcher creation. + SearcherStartCallbackKey ContextKey = "_searcher_start_callback_key" + SearcherEndCallbackKey ContextKey = "_searcher_end_callback_key" + + // FieldTermSynonymMapKey is used to store and transport the synonym definitions data + // to the actual search phase which would use the synonyms to perform the search. + FieldTermSynonymMapKey ContextKey = "_field_term_synonym_map_key" + + // BM25StatsKey is used to store and transport the BM25 Data + // to the actual search phase which would use it to perform the search. + BM25StatsKey ContextKey = "_bm25_stats_key" +) + +func RecordSearchCost(ctx context.Context, + msg SearchIncrementalCostCallbackMsg, bytes uint64, +) { + if ctx != nil { + queryType, ok := ctx.Value(QueryTypeKey).(SearchQueryType) + if !ok { + // for the cost of the non query type specific factors such as + // doc values and stored fields section. + queryType = GenericCost + } + + aggCallbackFn := ctx.Value(SearchIncrementalCostKey) + if aggCallbackFn != nil { + aggCallbackFn.(SearchIncrementalCostCallbackFn)(msg, queryType, bytes) + } + } +} + +// Assigning the size of the largest buffer in the pool to 24KB and +// the smallest buffer to 24 bytes. The pools are used to read a +// sequence of vertices which are always 24 bytes each. +const ( + MaxGeoBufPoolSize = 24 * 1024 + MinGeoBufPoolSize = 24 +) + +type GeoBufferPoolCallbackFunc func() *s2.GeoBufferPool + +// *PreSearchDataKey are used to store the data gathered during the presearch phase +// which would be use in the actual search phase. +const ( + KnnPreSearchDataKey = "_knn_pre_search_data_key" + SynonymPreSearchDataKey = "_synonym_pre_search_data_key" + BM25PreSearchDataKey = "_bm25_pre_search_data_key" +) + +const GlobalScoring = "_global_scoring" + +type ( + SearcherStartCallbackFn func(size uint64) error + SearcherEndCallbackFn func(size uint64) error +) + +type GetScoringModelCallbackFn func() string + +type ScoreExplCorrectionCallbackFunc func(queryMatch *DocumentMatch, knnMatch *DocumentMatch) (float64, *Explanation) + +// field -> term -> synonyms +type FieldTermSynonymMap map[string]map[string][]string + +func (f FieldTermSynonymMap) MergeWith(fts FieldTermSynonymMap) { + for field, termSynonymMap := range fts { + // Ensure the field exists in the receiver + if _, exists := f[field]; !exists { + f[field] = make(map[string][]string) + } + for term, synonyms := range termSynonymMap { + // Append synonyms + f[field][term] = append(f[field][term], synonyms...) + } + } +} + +// BM25 specific multipliers which control the scoring of a document. +// +// BM25_b - controls the extent to which doc's field length normalize term frequency part of score +// BM25_k1 - controls the saturation of the score due to term frequency +// the default values are as per elastic search's implementation +// - https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html#bm25 +// - https://www.elastic.co/blog/practical-bm25-part-3-considerations-for-picking-b-and-k1-in-elasticsearch +var ( + BM25_k1 float64 = 1.2 + BM25_b float64 = 0.75 +) + +type BM25Stats struct { + DocCount float64 `json:"doc_count"` + FieldCardinality map[string]int `json:"field_cardinality"` +} diff --git a/search/util_test.go b/search/util_test.go new file mode 100644 index 0000000..f65e3f9 --- /dev/null +++ b/search/util_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2013 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "reflect" + "testing" +) + +func TestMergeLocations(t *testing.T) { + flm1 := FieldTermLocationMap{ + "marty": TermLocationMap{ + "name": { + &Location{ + Pos: 1, + Start: 0, + End: 5, + }, + }, + }, + } + + flm2 := FieldTermLocationMap{ + "marty": TermLocationMap{ + "description": { + &Location{ + Pos: 5, + Start: 20, + End: 25, + }, + }, + }, + } + + flm3 := FieldTermLocationMap{ + "josh": TermLocationMap{ + "description": { + &Location{ + Pos: 5, + Start: 20, + End: 25, + }, + }, + }, + } + + expectedMerge := FieldTermLocationMap{ + "marty": TermLocationMap{ + "description": { + &Location{ + Pos: 5, + Start: 20, + End: 25, + }, + }, + "name": { + &Location{ + Pos: 1, + Start: 0, + End: 5, + }, + }, + }, + "josh": TermLocationMap{ + "description": { + &Location{ + Pos: 5, + Start: 20, + End: 25, + }, + }, + }, + } + + mergedLocations := MergeLocations([]FieldTermLocationMap{flm1, flm2, flm3}) + if !reflect.DeepEqual(expectedMerge, mergedLocations) { + t.Errorf("expected %v, got %v", expectedMerge, mergedLocations) + } +} diff --git a/search_knn.go b/search_knn.go new file mode 100644 index 0000000..1e6d52d --- /dev/null +++ b/search_knn.go @@ -0,0 +1,610 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package bleve + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/collector" + "github.com/blevesearch/bleve/v2/search/query" + index "github.com/blevesearch/bleve_index_api" +) + +const supportForVectorSearch = true + +type knnOperator string + +// Must be updated only at init +var BleveMaxK = int64(10000) + +type SearchRequest struct { + ClientContextID string `json:"client_context_id,omitempty"` + Query query.Query `json:"query"` + Size int `json:"size"` + From int `json:"from"` + Highlight *HighlightRequest `json:"highlight"` + Fields []string `json:"fields"` + Facets FacetsRequest `json:"facets"` + Explain bool `json:"explain"` + Sort search.SortOrder `json:"sort"` + IncludeLocations bool `json:"includeLocations"` + Score string `json:"score,omitempty"` + SearchAfter []string `json:"search_after"` + SearchBefore []string `json:"search_before"` + + KNN []*KNNRequest `json:"knn"` + KNNOperator knnOperator `json:"knn_operator"` + + // PreSearchData will be a map that will be used + // in the second phase of any 2-phase search, to provide additional + // context to the second phase. This is useful in the case of index + // aliases where the first phase will gather the PreSearchData from all + // the indexes in the alias, and the second phase will use that + // PreSearchData to perform the actual search. + // The currently accepted map configuration is: + // + // "_knn_pre_search_data_key": []*search.DocumentMatch + + PreSearchData map[string]interface{} `json:"pre_search_data,omitempty"` + + sortFunc func(sort.Interface) +} + +// Vector takes precedence over vectorBase64 in case both fields are given +type KNNRequest struct { + Field string `json:"field"` + Vector []float32 `json:"vector"` + VectorBase64 string `json:"vector_base64"` + K int64 `json:"k"` + Boost *query.Boost `json:"boost,omitempty"` + + // Search parameters for the field's vector index part of the segment. + // Value of it depends on the field's backing vector index implementation. + // + // For Faiss IVF index, supported search params are: + // - ivf_nprobe_pct : int // percentage of total clusters to search + // - ivf_max_codes_pct : float // percentage of total vectors to visit to do a query (across all clusters) + // + // Consult go-faiss to know all supported search params + Params json.RawMessage `json:"params"` + + // Filter query to use with kNN pre-filtering. + // Supports pre-filtering with all existing types of query clauses. + FilterQuery query.Query `json:"filter,omitempty"` +} + +func (r *SearchRequest) AddKNN(field string, vector []float32, k int64, boost float64) { + b := query.Boost(boost) + r.KNN = append(r.KNN, &KNNRequest{ + Field: field, + Vector: vector, + K: k, + Boost: &b, + }) +} + +func (r *SearchRequest) AddKNNWithFilter(field string, vector []float32, k int64, + boost float64, filterQuery query.Query) { + b := query.Boost(boost) + r.KNN = append(r.KNN, &KNNRequest{ + Field: field, + Vector: vector, + K: k, + Boost: &b, + FilterQuery: filterQuery, + }) +} + +func (r *SearchRequest) AddKNNOperator(operator knnOperator) { + r.KNNOperator = operator +} + +// UnmarshalJSON deserializes a JSON representation of +// a SearchRequest +func (r *SearchRequest) UnmarshalJSON(input []byte) error { + type tempKNNReq struct { + Field string `json:"field"` + Vector []float32 `json:"vector"` + VectorBase64 string `json:"vector_base64"` + K int64 `json:"k"` + Boost *query.Boost `json:"boost,omitempty"` + Params json.RawMessage `json:"params"` + FilterQuery json.RawMessage `json:"filter,omitempty"` + } + + var temp struct { + Q json.RawMessage `json:"query"` + Size *int `json:"size"` + From int `json:"from"` + Highlight *HighlightRequest `json:"highlight"` + Fields []string `json:"fields"` + Facets FacetsRequest `json:"facets"` + Explain bool `json:"explain"` + Sort []json.RawMessage `json:"sort"` + IncludeLocations bool `json:"includeLocations"` + Score string `json:"score"` + SearchAfter []string `json:"search_after"` + SearchBefore []string `json:"search_before"` + KNN []*tempKNNReq `json:"knn"` + KNNOperator knnOperator `json:"knn_operator"` + PreSearchData json.RawMessage `json:"pre_search_data"` + } + + err := json.Unmarshal(input, &temp) + if err != nil { + return err + } + + if temp.Size == nil { + r.Size = 10 + } else { + r.Size = *temp.Size + } + if temp.Sort == nil { + r.Sort = search.SortOrder{&search.SortScore{Desc: true}} + } else { + r.Sort, err = search.ParseSortOrderJSON(temp.Sort) + if err != nil { + return err + } + } + r.From = temp.From + r.Explain = temp.Explain + r.Highlight = temp.Highlight + r.Fields = temp.Fields + r.Facets = temp.Facets + r.IncludeLocations = temp.IncludeLocations + r.Score = temp.Score + r.SearchAfter = temp.SearchAfter + r.SearchBefore = temp.SearchBefore + r.Query, err = query.ParseQuery(temp.Q) + if err != nil { + return err + } + + if r.Size < 0 { + r.Size = 10 + } + if r.From < 0 { + r.From = 0 + } + + r.KNN = make([]*KNNRequest, len(temp.KNN)) + for i, knnReq := range temp.KNN { + r.KNN[i] = &KNNRequest{} + r.KNN[i].Field = temp.KNN[i].Field + r.KNN[i].Vector = temp.KNN[i].Vector + r.KNN[i].VectorBase64 = temp.KNN[i].VectorBase64 + r.KNN[i].K = temp.KNN[i].K + r.KNN[i].Boost = temp.KNN[i].Boost + r.KNN[i].Params = temp.KNN[i].Params + if len(knnReq.FilterQuery) == 0 { + // Setting this to nil to avoid ParseQuery() setting it to a match none + r.KNN[i].FilterQuery = nil + } else { + r.KNN[i].FilterQuery, err = query.ParseQuery(knnReq.FilterQuery) + if err != nil { + return err + } + } + } + r.KNNOperator = temp.KNNOperator + if r.KNNOperator == "" { + r.KNNOperator = knnOperatorOr + } + + if temp.PreSearchData != nil { + r.PreSearchData, err = query.ParsePreSearchData(temp.PreSearchData) + if err != nil { + return err + } + } + + return nil + +} + +// ----------------------------------------------------------------------------- + +func copySearchRequest(req *SearchRequest, preSearchData map[string]interface{}) *SearchRequest { + rv := SearchRequest{ + Query: req.Query, + Size: req.Size + req.From, + From: 0, + Highlight: req.Highlight, + Fields: req.Fields, + Facets: req.Facets, + Explain: req.Explain, + Sort: req.Sort.Copy(), + IncludeLocations: req.IncludeLocations, + Score: req.Score, + SearchAfter: req.SearchAfter, + SearchBefore: req.SearchBefore, + KNN: req.KNN, + KNNOperator: req.KNNOperator, + PreSearchData: preSearchData, + } + return &rv + +} + +var ( + knnOperatorAnd = knnOperator("and") + knnOperatorOr = knnOperator("or") +) + +func createKNNQuery(req *SearchRequest, knnFilterResults map[int]index.EligibleDocumentSelector) ( + query.Query, []int64, int64, error) { + if requestHasKNN(req) { + // first perform validation + err := validateKNN(req) + if err != nil { + return nil, nil, 0, err + } + var subQueries []query.Query + kArray := make([]int64, 0, len(req.KNN)) + sumOfK := int64(0) + for i, knn := range req.KNN { + // If it's a filtered kNN but has no eligible filter hits, then + // do not run the kNN query. + if selector, exists := knnFilterResults[i]; exists && selector == nil { + continue + } + knnQuery := query.NewKNNQuery(knn.Vector) + knnQuery.SetFieldVal(knn.Field) + knnQuery.SetK(knn.K) + knnQuery.SetBoost(knn.Boost.Value()) + knnQuery.SetParams(knn.Params) + if selector, exists := knnFilterResults[i]; exists { + knnQuery.SetEligibleSelector(selector) + } + subQueries = append(subQueries, knnQuery) + kArray = append(kArray, knn.K) + sumOfK += knn.K + } + rv := query.NewDisjunctionQuery(subQueries) + rv.RetrieveScoreBreakdown(true) + return rv, kArray, sumOfK, nil + } + return nil, nil, 0, nil +} + +func validateKNN(req *SearchRequest) error { + for _, q := range req.KNN { + if q == nil { + return fmt.Errorf("knn query cannot be nil") + } + if len(q.Vector) == 0 && q.VectorBase64 != "" { + // consider vector_base64 only if vector is not provided + decodedVector, err := document.DecodeVector(q.VectorBase64) + if err != nil { + return err + } + + q.Vector = decodedVector + } + if q.K <= 0 || len(q.Vector) == 0 { + return fmt.Errorf("k must be greater than 0 and vector must be non-empty") + } + if q.K > BleveMaxK { + return fmt.Errorf("k must be less than %d", BleveMaxK) + } + // since the DefaultField is not applicable for knn, + // the field must be specified. + if q.Field == "" { + return fmt.Errorf("knn query field must be non-empty") + } + if vfq, ok := q.FilterQuery.(query.ValidatableQuery); ok { + err := vfq.Validate() + if err != nil { + return fmt.Errorf("knn filter query is invalid: %v", err) + } + } + } + switch req.KNNOperator { + case knnOperatorAnd, knnOperatorOr, "": + // Valid cases, do nothing + default: + return fmt.Errorf("knn_operator must be either 'and' / 'or'") + } + return nil +} + +func addSortAndFieldsToKNNHits(req *SearchRequest, knnHits []*search.DocumentMatch, reader index.IndexReader, name string) (err error) { + requiredSortFields := req.Sort.RequiredFields() + var dvReader index.DocValueReader + var updateFieldVisitor index.DocValueVisitor + if len(requiredSortFields) > 0 { + dvReader, err = reader.DocValueReader(requiredSortFields) + if err != nil { + return err + } + updateFieldVisitor = func(field string, term []byte) { + req.Sort.UpdateVisitor(field, term) + } + } + for _, hit := range knnHits { + if len(requiredSortFields) > 0 { + err = dvReader.VisitDocValues(hit.IndexInternalID, updateFieldVisitor) + if err != nil { + return err + } + } + req.Sort.Value(hit) + err, _ = LoadAndHighlightFields(hit, req, "", reader, nil) + if err != nil { + return err + } + hit.Index = name + } + return nil +} + +func (i *indexImpl) runKnnCollector(ctx context.Context, req *SearchRequest, reader index.IndexReader, preSearch bool) ([]*search.DocumentMatch, error) { + // Maps the index of a KNN query in the request to its pre-filter result: + // - If the KNN query is **not filtered**, the value will be `nil`. + // - If the KNN query **is filtered**, the value will be an eligible document selector + // that can be used to retrieve eligible documents. + // - If there is an **empty entry** for a KNN query, it means no documents match + // the filter query, and the KNN query can be skipped. + knnFilterResults := make(map[int]index.EligibleDocumentSelector) + for idx, knnReq := range req.KNN { + filterQ := knnReq.FilterQuery + if filterQ == nil || isMatchAllQuery(filterQ) { + // When there is no filter query or the filter query is match_all, + // all documents are eligible, and can be treated as unfiltered query. + continue + } else if isMatchNoneQuery(filterQ) { + // If the filter query is match_none, then no documents match the filter query. + knnFilterResults[idx] = nil + continue + } + // Applies to all supported types of queries. + filterSearcher, _ := filterQ.Searcher(ctx, reader, i.m, search.SearcherOptions{ + Score: "none", // just want eligible hits --> don't compute scores if not needed + }) + // Using the index doc count to determine collector size since we do not + // have an estimate of the number of eligible docs in the index yet. + indexDocCount, err := i.DocCount() + if err != nil { + return nil, err + } + filterColl := collector.NewEligibleCollector(int(indexDocCount)) + err = filterColl.Collect(ctx, filterSearcher, reader) + if err != nil { + return nil, err + } + knnFilterResults[idx] = filterColl.EligibleSelector() + } + + // Add the filter hits when creating the kNN query + KNNQuery, kArray, sumOfK, err := createKNNQuery(req, knnFilterResults) + if err != nil { + return nil, err + } + knnSearcher, err := KNNQuery.Searcher(ctx, reader, i.m, search.SearcherOptions{ + Explain: req.Explain, + }) + if err != nil { + return nil, err + } + knnCollector := collector.NewKNNCollector(kArray, sumOfK) + err = knnCollector.Collect(ctx, knnSearcher, reader) + if err != nil { + return nil, err + } + knnHits := knnCollector.Results() + if !preSearch { + knnHits = finalizeKNNResults(req, knnHits) + } + // at this point, irrespective of whether it is a preSearch or not, + // the knn hits are populated with Sort and Fields. + // it must be ensured downstream that the Sort and Fields are not + // re-evaluated, for these hits. + // also add the index names to the hits, so that when early + // exit takes place after the first phase, the hits will have + // a valid value for Index. + err = addSortAndFieldsToKNNHits(req, knnHits, reader, i.name) + if err != nil { + return nil, err + } + return knnHits, nil +} + +func setKnnHitsInCollector(knnHits []*search.DocumentMatch, req *SearchRequest, coll *collector.TopNCollector) { + if len(knnHits) > 0 { + newScoreExplComputer := func(queryMatch *search.DocumentMatch, knnMatch *search.DocumentMatch) (float64, *search.Explanation) { + totalScore := queryMatch.Score + knnMatch.Score + if !req.Explain { + // exit early as we don't need to compute the explanation + return totalScore, nil + } + return totalScore, &search.Explanation{Value: totalScore, Message: "sum of:", Children: []*search.Explanation{queryMatch.Expl, knnMatch.Expl}} + } + coll.SetKNNHits(knnHits, search.ScoreExplCorrectionCallbackFunc(newScoreExplComputer)) + } +} + +func finalizeKNNResults(req *SearchRequest, knnHits []*search.DocumentMatch) []*search.DocumentMatch { + // if the KNN operator is AND, then we need to filter out the hits that + // do not have match the KNN queries. + if req.KNNOperator == knnOperatorAnd { + idx := 0 + for _, hit := range knnHits { + if len(hit.ScoreBreakdown) == len(req.KNN) { + knnHits[idx] = hit + idx++ + } + } + knnHits = knnHits[:idx] + } + // fix the score using score breakdown now + // if the score is none, then we need to set the score to 0.0 + // if req.Explain is true, then we need to use the expl breakdown to + // finalize the correct explanation. + for _, hit := range knnHits { + hit.Score = 0.0 + if req.Score != "none" { + for _, score := range hit.ScoreBreakdown { + hit.Score += score + } + } + if req.Explain { + childrenExpl := make([]*search.Explanation, 0, len(hit.ScoreBreakdown)) + for i := range hit.ScoreBreakdown { + childrenExpl = append(childrenExpl, hit.Expl.Children[i]) + } + hit.Expl = &search.Explanation{Value: hit.Score, Message: "sum of:", Children: childrenExpl} + } + // we don't need the score breakdown anymore + // so we can set it to nil + hit.ScoreBreakdown = nil + } + return knnHits +} + +// when we are setting KNN hits in the preSearchData, we need to make sure that +// the KNN hit goes to the right index. This is because the KNN hits are +// collected from all the indexes in the alias, but the preSearchData is +// specific to each index. If alias A1 contains indexes I1 and I2 and +// the KNN hits collected from both I1 and I2, and merged to get top K +// hits, then the top K hits need to be distributed to I1 and I2, +// so that the preSearchData for I1 contains the top K hits from I1 and +// the preSearchData for I2 contains the top K hits from I2. +func validateAndDistributeKNNHits(knnHits []*search.DocumentMatch, indexes []Index) (map[string][]*search.DocumentMatch, error) { + // create a set of all the index names of this alias + indexNames := make(map[string]struct{}, len(indexes)) + for _, index := range indexes { + indexNames[index.Name()] = struct{}{} + } + segregatedKnnHits := make(map[string][]*search.DocumentMatch) + for _, hit := range knnHits { + // for each hit, we need to perform a validation check to ensure that the stack + // is still valid. + // + // if the stack is empty, then we have an inconsistency/abnormality + // since any hit with an empty stack is supposed to land on a leaf index, + // and not an alias. This cannot happen in normal circumstances. But + // performing this check to be safe. Since we extract the stack top + // in the following steps. + if len(hit.IndexNames) == 0 { + return nil, ErrorTwoPhaseSearchInconsistency + } + // since the stack is not empty, we need to check if the top of the stack + // is a valid index name, of an index that is part of this alias. If not, + // then we have an inconsistency that could be caused due to a topology + // change. + stackTopIdx := len(hit.IndexNames) - 1 + top := hit.IndexNames[stackTopIdx] + if _, exists := indexNames[top]; !exists { + return nil, ErrorTwoPhaseSearchInconsistency + } + if stackTopIdx == 0 { + // if the stack consists of only one index, then popping the top + // would result in an empty slice, and handle this case by setting + // indexNames to nil. So that the final search results will not + // contain the indexNames field. + hit.IndexNames = nil + } else { + hit.IndexNames = hit.IndexNames[:stackTopIdx] + } + segregatedKnnHits[top] = append(segregatedKnnHits[top], hit) + } + return segregatedKnnHits, nil +} + +func requestHasKNN(req *SearchRequest) bool { + return len(req.KNN) > 0 +} + +// returns true if the search request contains a KNN request that can be +// satisfied by just performing a preSearch, completely bypassing the +// actual search. +func isKNNrequestSatisfiedByPreSearch(req *SearchRequest) bool { + // if req.Query is not match_none => then we need to go to phase 2 + // to perform the actual query. + if !isMatchNoneQuery(req.Query) { + return false + } + // req.Query is a match_none query + // + // if request contains facets, we need to perform phase 2 to calculate + // the facet result. Since documents were removed as part of the + // merging process after phase 1, if the facet results were to be calculated + // during phase 1, then they will be now be incorrect, since merging would + // remove some documents. + if req.Facets != nil { + return false + } + // the request is a match_none query and does not contain any facets + // so we can satisfy the request using just the preSearch result. + return true +} + +func constructKnnPreSearchData(mergedOut map[string]map[string]interface{}, preSearchResult *SearchResult, + indexes []Index) (map[string]map[string]interface{}, error) { + + distributedHits, err := validateAndDistributeKNNHits([]*search.DocumentMatch(preSearchResult.Hits), indexes) + if err != nil { + return nil, err + } + for _, index := range indexes { + mergedOut[index.Name()][search.KnnPreSearchDataKey] = distributedHits[index.Name()] + } + return mergedOut, nil +} + +func addKnnToDummyRequest(dummyReq *SearchRequest, realReq *SearchRequest) { + dummyReq.KNN = realReq.KNN + dummyReq.KNNOperator = knnOperatorOr + dummyReq.Explain = realReq.Explain + dummyReq.Fields = realReq.Fields + dummyReq.Sort = realReq.Sort +} + +func newKnnPreSearchResultProcessor(req *SearchRequest) *knnPreSearchResultProcessor { + kArray := make([]int64, len(req.KNN)) + for i, knnReq := range req.KNN { + kArray[i] = knnReq.K + } + knnStore := collector.GetNewKNNCollectorStore(kArray) + return &knnPreSearchResultProcessor{ + addFn: func(sr *SearchResult, indexName string) { + for _, hit := range sr.Hits { + // tag the hit with the index name, so that when the + // final search result is constructed, the hit will have + // a valid path to follow along the alias tree to reach + // the index. + hit.IndexNames = append(hit.IndexNames, indexName) + knnStore.AddDocument(hit) + } + }, + finalizeFn: func(sr *SearchResult) { + // passing nil as the document fixup function, because we don't need to + // fixup the document, since this was already done in the first phase, + // hence error is always nil. + // the merged knn hits are finalized and set in the search result. + sr.Hits, _ = knnStore.Final(nil) + }, + } +} diff --git a/search_knn_test.go b/search_knn_test.go new file mode 100644 index 0000000..a2d207b --- /dev/null +++ b/search_knn_test.go @@ -0,0 +1,1703 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build vectors +// +build vectors + +package bleve + +import ( + "archive/zip" + "bytes" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "math" + "math/rand" + "sort" + "strconv" + "sync" + "testing" + + "github.com/blevesearch/bleve/v2/analysis/lang/en" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/query" + index "github.com/blevesearch/bleve_index_api" +) + +const testInputCompressedFile = "test/knn/knn_dataset_queries.zip" +const testDatasetFileName = "knn_dataset.json" +const testQueryFileName = "knn_queries.json" + +const testDatasetDims = 384 + +var knnOperators []knnOperator = []knnOperator{knnOperatorAnd, knnOperatorOr} + +func TestSimilaritySearchPartitionedIndex(t *testing.T) { + dataset, searchRequests, err := readDatasetAndQueries(testInputCompressedFile) + if err != nil { + t.Fatal(err) + } + documents := makeDatasetIntoDocuments(dataset) + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Analyzer = en.AnalyzerName + + vecFieldMappingL2 := mapping.NewVectorFieldMapping() + vecFieldMappingL2.Dims = testDatasetDims + vecFieldMappingL2.Similarity = index.EuclideanDistance + + indexMappingL2Norm := NewIndexMapping() + indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping) + indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2) + + vecFieldMappingDot := mapping.NewVectorFieldMapping() + vecFieldMappingDot.Dims = testDatasetDims + vecFieldMappingDot.Similarity = index.InnerProduct + + indexMappingDotProduct := NewIndexMapping() + indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping) + indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingDot) + + vecFieldMappingCosine := mapping.NewVectorFieldMapping() + vecFieldMappingCosine.Dims = testDatasetDims + vecFieldMappingCosine.Similarity = index.CosineSimilarity + + indexMappingCosine := NewIndexMapping() + indexMappingCosine.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping) + indexMappingCosine.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingCosine) + + type testCase struct { + testType string + queryIndex int + numIndexPartitions int + mapping mapping.IndexMapping + } + + testCases := []testCase{ + // l2 norm similarity + { + testType: "multi_partition:match_none:oneKNNreq:k=3", + queryIndex: 0, + numIndexPartitions: 4, + mapping: indexMappingL2Norm, + }, + { + testType: "multi_partition:match_none:oneKNNreq:k=2", + queryIndex: 0, + numIndexPartitions: 10, + mapping: indexMappingL2Norm, + }, + { + testType: "multi_partition:match:oneKNNreq:k=2", + queryIndex: 1, + numIndexPartitions: 5, + mapping: indexMappingL2Norm, + }, + { + testType: "multi_partition:disjunction:twoKNNreq:k=2,2", + queryIndex: 2, + numIndexPartitions: 4, + mapping: indexMappingL2Norm, + }, + // dot product similarity + { + testType: "multi_partition:match_none:oneKNNreq:k=3", + queryIndex: 0, + numIndexPartitions: 4, + mapping: indexMappingDotProduct, + }, + { + testType: "multi_partition:match_none:oneKNNreq:k=2", + queryIndex: 0, + numIndexPartitions: 10, + mapping: indexMappingDotProduct, + }, + { + testType: "multi_partition:match:oneKNNreq:k=2", + queryIndex: 1, + numIndexPartitions: 5, + mapping: indexMappingDotProduct, + }, + { + testType: "multi_partition:disjunction:twoKNNreq:k=2,2", + queryIndex: 2, + numIndexPartitions: 4, + mapping: indexMappingDotProduct, + }, + // cosine similarity + { + testType: "multi_partition:match_none:oneKNNreq:k=3", + queryIndex: 0, + numIndexPartitions: 7, + mapping: indexMappingCosine, + }, + { + testType: "multi_partition:match_none:oneKNNreq:k=2", + queryIndex: 0, + numIndexPartitions: 5, + mapping: indexMappingCosine, + }, + { + testType: "multi_partition:match:oneKNNreq:k=2", + queryIndex: 1, + numIndexPartitions: 3, + mapping: indexMappingCosine, + }, + { + testType: "multi_partition:disjunction:twoKNNreq:k=2,2", + queryIndex: 2, + numIndexPartitions: 9, + mapping: indexMappingCosine, + }, + } + + index := NewIndexAlias() + var reqSort = search.SortOrder{&search.SortScore{Desc: true}, &search.SortDocID{Desc: true}, &search.SortField{Desc: false, Field: "content"}} + for testCaseNum, testCase := range testCases { + originalRequest := searchRequests[testCase.queryIndex] + for _, operator := range knnOperators { + + index.indexes = make([]Index, 0) + query := copySearchRequest(originalRequest, nil) + query.AddKNNOperator(operator) + query.Sort = reqSort.Copy() + query.Explain = true + + nameToIndex := createPartitionedIndex(documents, index, 1, testCase.mapping, t, false) + controlResult, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(controlResult.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected control result hits to have valid `Index`", testCaseNum) + } + cleanUp(t, nameToIndex) + + index.indexes = make([]Index, 0) + query = copySearchRequest(originalRequest, nil) + query.AddKNNOperator(operator) + query.Sort = reqSort.Copy() + query.Explain = true + + nameToIndex = createPartitionedIndex(documents, index, testCase.numIndexPartitions, testCase.mapping, t, false) + experimentalResult, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(experimentalResult.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected experimental Result hits to have valid `Index`", testCaseNum) + } + verifyResult(t, controlResult, experimentalResult, testCaseNum, true) + cleanUp(t, nameToIndex) + + index.indexes = make([]Index, 0) + query = copySearchRequest(originalRequest, nil) + query.AddKNNOperator(operator) + query.Sort = reqSort.Copy() + query.Explain = true + + nameToIndex = createPartitionedIndex(documents, index, testCase.numIndexPartitions, testCase.mapping, t, true) + multiLevelIndexResult, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(multiLevelIndexResult.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected experimental Result hits to have valid `Index`", testCaseNum) + } + verifyResult(t, multiLevelIndexResult, experimentalResult, testCaseNum, false) + cleanUp(t, nameToIndex) + + } + } + + var facets = map[string]*FacetRequest{ + "content": { + Field: "content", + Size: 10, + }, + } + + index = NewIndexAlias() + for testCaseNum, testCase := range testCases { + index.indexes = make([]Index, 0) + nameToIndex := createPartitionedIndex(documents, index, testCase.numIndexPartitions, testCase.mapping, t, false) + originalRequest := searchRequests[testCase.queryIndex] + for _, operator := range knnOperators { + from, size := originalRequest.From, originalRequest.Size + query := copySearchRequest(originalRequest, nil) + query.AddKNNOperator(operator) + query.Explain = true + query.From = from + query.Size = size + + // Three types of queries to run wrt sort and facet fields that require fields. + // 1. Sort And Facet are there + // 2. Sort is there, Facet is not there + // 3. Sort is not there, Facet is there + // The case where both sort and facet are not there is already covered in the previous tests. + + // 1. Sort And Facet are there + query.Facets = facets + query.Sort = reqSort.Copy() + + res1, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(res1.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected experimental Result hits to have valid `Index`", testCaseNum) + } + + facetRes1 := res1.Facets + facetRes1Str, err := json.Marshal(facetRes1) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + + // 2. Sort is there, Facet is not there + query.Facets = nil + query.Sort = reqSort.Copy() + + res2, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(res2.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected experimental Result hits to have valid `Index`", testCaseNum) + } + + // 3. Sort is not there, Facet is there + query.Facets = facets + query.Sort = nil + res3, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(res3.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected experimental Result hits to have valid `Index`", testCaseNum) + } + + facetRes3 := res3.Facets + facetRes3Str, err := json.Marshal(facetRes3) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + + // Verify the facet results + if string(facetRes1Str) != string(facetRes3Str) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected facet results to be equal", testCaseNum) + } + + // Verify the results + verifyResult(t, res1, res2, testCaseNum, false) + verifyResult(t, res2, res3, testCaseNum, true) + + // Test early exit fail case -> matchNone + facetRequest + query.Query = NewMatchNoneQuery() + query.Sort = reqSort.Copy() + // control case + query.Facets = nil + res4Ctrl, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(res4Ctrl.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected control Result hits to have valid `Index`", testCaseNum) + } + + // experimental case + query.Facets = facets + res4Exp, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(res4Exp.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected experimental Result hits to have valid `Index`", testCaseNum) + } + + if !(operator == knnOperatorAnd && res4Ctrl.Total == 0 && res4Exp.Total == 0) { + // catch case where no hits are returned + // due to matchNone query with a KNN request with operator AND + // where no hits are part of the intersection in multi knn request + verifyResult(t, res4Ctrl, res4Exp, testCaseNum, false) + } + } + cleanUp(t, nameToIndex) + } + + // Test Pagination with multi partitioned index + index = NewIndexAlias() + index.indexes = make([]Index, 0) + nameToIndex := createPartitionedIndex(documents, index, 8, indexMappingL2Norm, t, true) + + // Test From + Size pagination for Hybrid Search (2-Phase) + query := copySearchRequest(searchRequests[4], nil) + query.Sort = reqSort.Copy() + query.Facets = facets + query.Explain = true + + testFromSizePagination(t, query, index, nameToIndex) + + // Test From + Size pagination for Early Exit Hybrid Search (1-Phase) + query = copySearchRequest(searchRequests[4], nil) + query.Query = NewMatchNoneQuery() + query.Sort = reqSort.Copy() + query.Facets = nil + query.Explain = true + + testFromSizePagination(t, query, index, nameToIndex) + + cleanUp(t, nameToIndex) +} + +func testFromSizePagination(t *testing.T, query *SearchRequest, index Index, nameToIndex map[string]Index) { + query.From = 0 + query.Size = 30 + + resCtrl, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + + ctrlHitIds := make([]string, len(resCtrl.Hits)) + for i, doc := range resCtrl.Hits { + ctrlHitIds[i] = doc.ID + } + // experimental case + + fromValues := []int{0, 5, 10, 15, 20, 25} + size := 5 + for fromIdx := 0; fromIdx < len(fromValues); fromIdx++ { + from := fromValues[fromIdx] + query.From = from + query.Size = size + resExp, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if from >= len(ctrlHitIds) { + if len(resExp.Hits) != 0 { + cleanUp(t, nameToIndex) + t.Fatalf("expected 0 hits, got %d", len(resExp.Hits)) + } + continue + } + numHitsExp := len(resExp.Hits) + numHitsCtrl := min(len(ctrlHitIds)-from, size) + if numHitsExp != numHitsCtrl { + cleanUp(t, nameToIndex) + t.Fatalf("expected %d hits, got %d", numHitsCtrl, numHitsExp) + } + for i := 0; i < numHitsExp; i++ { + doc := resExp.Hits[i] + startOffset := from + i + if doc.ID != ctrlHitIds[startOffset] { + cleanUp(t, nameToIndex) + t.Fatalf("expected %s at index %d, got %s", ctrlHitIds[startOffset], i, doc.ID) + } + } + } +} + +func TestVectorBase64Index(t *testing.T) { + dataset, searchRequests, err := readDatasetAndQueries(testInputCompressedFile) + if err != nil { + t.Fatal(err) + } + documents := makeDatasetIntoDocuments(dataset) + + _, searchRequestsCopy, err := readDatasetAndQueries(testInputCompressedFile) + if err != nil { + t.Fatal(err) + } + + for _, doc := range documents { + vec, ok := doc["vector"].([]float32) + if !ok { + t.Fatal("Typecasting vector to float array failed") + } + + buf := new(bytes.Buffer) + for _, v := range vec { + err := binary.Write(buf, binary.LittleEndian, v) + if err != nil { + t.Fatal(err) + } + } + + doc["vectorEncoded"] = base64.StdEncoding.EncodeToString(buf.Bytes()) + } + + for _, sr := range searchRequestsCopy { + for _, kr := range sr.KNN { + kr.Field = "vectorEncoded" + } + } + + contentFM := NewTextFieldMapping() + contentFM.Analyzer = en.AnalyzerName + + vecFML2 := mapping.NewVectorFieldMapping() + vecFML2.Dims = testDatasetDims + vecFML2.Similarity = index.EuclideanDistance + + vecBFML2 := mapping.NewVectorBase64FieldMapping() + vecBFML2.Dims = testDatasetDims + vecBFML2.Similarity = index.EuclideanDistance + + vecFMDot := mapping.NewVectorFieldMapping() + vecFMDot.Dims = testDatasetDims + vecFMDot.Similarity = index.InnerProduct + + vecBFMDot := mapping.NewVectorBase64FieldMapping() + vecBFMDot.Dims = testDatasetDims + vecBFMDot.Similarity = index.InnerProduct + + indexMappingL2 := NewIndexMapping() + indexMappingL2.DefaultMapping.AddFieldMappingsAt("content", contentFM) + indexMappingL2.DefaultMapping.AddFieldMappingsAt("vector", vecFML2) + indexMappingL2.DefaultMapping.AddFieldMappingsAt("vectorEncoded", vecBFML2) + + indexMappingDot := NewIndexMapping() + indexMappingDot.DefaultMapping.AddFieldMappingsAt("content", contentFM) + indexMappingDot.DefaultMapping.AddFieldMappingsAt("vector", vecFMDot) + indexMappingDot.DefaultMapping.AddFieldMappingsAt("vectorEncoded", vecBFMDot) + + tmpIndexPathL2 := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPathL2) + + tmpIndexPathDot := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPathDot) + + indexL2, err := New(tmpIndexPathL2, indexMappingL2) + if err != nil { + t.Fatal(err) + } + defer func() { + err := indexL2.Close() + if err != nil { + t.Fatal(err) + } + }() + + indexDot, err := New(tmpIndexPathDot, indexMappingDot) + if err != nil { + t.Fatal(err) + } + defer func() { + err := indexDot.Close() + if err != nil { + t.Fatal(err) + } + }() + + batchL2 := indexL2.NewBatch() + batchDot := indexDot.NewBatch() + + for _, doc := range documents { + err = batchL2.Index(doc["id"].(string), doc) + if err != nil { + t.Fatal(err) + } + err = batchDot.Index(doc["id"].(string), doc) + if err != nil { + t.Fatal(err) + } + } + + err = indexL2.Batch(batchL2) + if err != nil { + t.Fatal(err) + } + + err = indexDot.Batch(batchDot) + if err != nil { + t.Fatal(err) + } + + for i := range searchRequests { + for _, operator := range knnOperators { + controlQuery := searchRequests[i] + testQuery := searchRequestsCopy[i] + + controlQuery.AddKNNOperator(operator) + testQuery.AddKNNOperator(operator) + + controlResultL2, err := indexL2.Search(controlQuery) + if err != nil { + t.Fatal(err) + } + testResultL2, err := indexL2.Search(testQuery) + if err != nil { + t.Fatal(err) + } + + if controlResultL2 != nil && testResultL2 != nil { + if len(controlResultL2.Hits) == len(testResultL2.Hits) { + for j := range controlResultL2.Hits { + if controlResultL2.Hits[j].ID != testResultL2.Hits[j].ID { + t.Fatalf("testcase %d failed: expected hit id %s, got hit id %s", i, controlResultL2.Hits[j].ID, testResultL2.Hits[j].ID) + } + } + } + } else if (controlResultL2 == nil && testResultL2 != nil) || + (controlResultL2 != nil && testResultL2 == nil) { + t.Fatalf("testcase %d failed: expected result %s, got result %s", i, controlResultL2, testResultL2) + } + + controlResultDot, err := indexDot.Search(controlQuery) + if err != nil { + t.Fatal(err) + } + testResultDot, err := indexDot.Search(testQuery) + if err != nil { + t.Fatal(err) + } + + if controlResultDot != nil && testResultDot != nil { + if len(controlResultDot.Hits) == len(testResultDot.Hits) { + for j := range controlResultDot.Hits { + if controlResultDot.Hits[j].ID != testResultDot.Hits[j].ID { + t.Fatalf("testcase %d failed: expected hit id %s, got hit id %s", i, controlResultDot.Hits[j].ID, testResultDot.Hits[j].ID) + } + } + } + } else if (controlResultDot == nil && testResultDot != nil) || + (controlResultDot != nil && testResultDot == nil) { + t.Fatalf("testcase %d failed: expected result %s, got result %s", i, controlResultDot, testResultDot) + } + } + } +} + +type testDocument struct { + ID string `json:"id"` + Content string `json:"content"` + Vector []float32 `json:"vector"` +} + +func readDatasetAndQueries(fileName string) ([]testDocument, []*SearchRequest, error) { + // Open the zip archive for reading + r, err := zip.OpenReader(fileName) + if err != nil { + return nil, nil, err + } + var dataset []testDocument + var queries []*SearchRequest + + defer r.Close() + for _, f := range r.File { + jsonFile, err := f.Open() + if err != nil { + return nil, nil, err + } + defer jsonFile.Close() + if f.Name == testDatasetFileName { + err = json.NewDecoder(jsonFile).Decode(&dataset) + if err != nil { + return nil, nil, err + } + } else if f.Name == testQueryFileName { + err = json.NewDecoder(jsonFile).Decode(&queries) + if err != nil { + return nil, nil, err + } + } + } + return dataset, queries, nil +} + +func makeDatasetIntoDocuments(dataset []testDocument) []map[string]interface{} { + documents := make([]map[string]interface{}, len(dataset)) + for i := 0; i < len(dataset); i++ { + document := make(map[string]interface{}) + document["id"] = dataset[i].ID + document["content"] = dataset[i].Content + document["vector"] = dataset[i].Vector + documents[i] = document + } + return documents +} + +func cleanUp(t *testing.T, nameToIndex map[string]Index) { + for path, childIndex := range nameToIndex { + err := childIndex.Close() + if err != nil { + t.Fatal(err) + } + cleanupTmpIndexPath(t, path) + } +} + +func createChildIndex(docs []map[string]interface{}, mapping mapping.IndexMapping, t *testing.T, nameToIndex map[string]Index) Index { + tmpIndexPath := createTmpIndexPath(t) + index, err := New(tmpIndexPath, mapping) + if err != nil { + t.Fatal(err) + } + nameToIndex[index.Name()] = index + batch := index.NewBatch() + for _, doc := range docs { + err := batch.Index(doc["id"].(string), doc) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + } + err = index.Batch(batch) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + return index +} + +func createPartitionedIndex(documents []map[string]interface{}, index *indexAliasImpl, numPartitions int, + mapping mapping.IndexMapping, t *testing.T, multiLevel bool) map[string]Index { + + partitionSize := len(documents) / numPartitions + extraDocs := len(documents) % numPartitions + numDocsPerPartition := make([]int, numPartitions) + for i := 0; i < numPartitions; i++ { + numDocsPerPartition[i] = partitionSize + if extraDocs > 0 { + numDocsPerPartition[i]++ + extraDocs-- + } + } + docsPerPartition := make([][]map[string]interface{}, numPartitions) + prevCutoff := 0 + for i := 0; i < numPartitions; i++ { + docsPerPartition[i] = make([]map[string]interface{}, numDocsPerPartition[i]) + for j := 0; j < numDocsPerPartition[i]; j++ { + docsPerPartition[i][j] = documents[prevCutoff+j] + } + prevCutoff += numDocsPerPartition[i] + } + + rv := make(map[string]Index) + if !multiLevel { + // all indexes are at the same level + for i := 0; i < numPartitions; i++ { + index.Add(createChildIndex(docsPerPartition[i], mapping, t, rv)) + } + } else { + // alias tree + indexes := make([]Index, numPartitions) + for i := 0; i < numPartitions; i++ { + indexes[i] = createChildIndex(docsPerPartition[i], mapping, t, rv) + } + numAlias := int(math.Ceil(float64(numPartitions) / 2.0)) + aliases := make([]IndexAlias, numAlias) + for i := 0; i < numAlias; i++ { + aliases[i] = NewIndexAlias() + aliases[i].SetName(fmt.Sprintf("alias%d", i)) + for j := 0; j < 2; j++ { + if i*2+j < numPartitions { + aliases[i].Add(indexes[i*2+j]) + } + } + } + for i := 0; i < numAlias; i++ { + index.Add(aliases[i]) + } + } + return rv +} + +func createMultipleSegmentsIndex(documents []map[string]interface{}, index Index, numSegments int) error { + // create multiple batches to simulate more than one segment + numBatches := numSegments + + batches := make([]*Batch, numBatches) + numDocsPerBatch := len(documents) / numBatches + extraDocs := len(documents) % numBatches + + docsPerBatch := make([]int, numBatches) + for i := 0; i < numBatches; i++ { + docsPerBatch[i] = numDocsPerBatch + if extraDocs > 0 { + docsPerBatch[i]++ + extraDocs-- + } + } + prevCutoff := 0 + for i := 0; i < numBatches; i++ { + batches[i] = index.NewBatch() + for j := prevCutoff; j < prevCutoff+docsPerBatch[i]; j++ { + doc := documents[j] + err := batches[i].Index(doc["id"].(string), doc) + if err != nil { + return err + } + } + prevCutoff += docsPerBatch[i] + } + errMutex := sync.Mutex{} + var errors []error + wg := sync.WaitGroup{} + wg.Add(len(batches)) + for i, batch := range batches { + go func(ix int, batchx *Batch) { + defer wg.Done() + err := index.Batch(batchx) + if err != nil { + errMutex.Lock() + errors = append(errors, err) + errMutex.Unlock() + } + }(i, batch) + } + wg.Wait() + if len(errors) > 0 { + return errors[0] + } + return nil +} + +func truncateScore(score float64) float64 { + epsilon := 1e-4 + truncated := float64(int(score*1e6)) / 1e6 + if math.Abs(truncated-1.0) <= epsilon { + return 1.0 + } + return truncated +} + +// Function to compare two Explanation structs recursively +func compareExplanation(a, b *search.Explanation) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + + if truncateScore(a.Value) != truncateScore(b.Value) || len(a.Children) != len(b.Children) { + return false + } + + // Sort the children slices before comparison + sortChildren(a.Children) + sortChildren(b.Children) + + for i := range a.Children { + if !compareExplanation(a.Children[i], b.Children[i]) { + return false + } + } + return true +} + +// Function to sort the children slices +func sortChildren(children []*search.Explanation) { + sort.Slice(children, func(i, j int) bool { + return children[i].Value < children[j].Value + }) +} + +// All hits from a hybrid search/knn search should not have +// index names or score breakdown. +func finalHitsOmitKNNMetadata(hits []*search.DocumentMatch) bool { + for _, hit := range hits { + if hit.IndexNames != nil || hit.ScoreBreakdown != nil { + return false + } + } + return true +} + +func finalHitsHaveValidIndex(hits []*search.DocumentMatch, indexes map[string]Index) bool { + for _, hit := range hits { + if hit.Index == "" { + return false + } + var idx Index + var ok bool + if idx, ok = indexes[hit.Index]; !ok { + return false + } + if idx == nil { + return false + } + var doc index.Document + doc, err = idx.Document(hit.ID) + if err != nil { + return false + } + if doc == nil { + return false + } + } + return true +} + +func verifyResult(t *testing.T, controlResult *SearchResult, experimentalResult *SearchResult, testCaseNum int, verifyOnlyDocIDs bool) { + if controlResult.Hits.Len() == 0 || experimentalResult.Hits.Len() == 0 { + t.Fatalf("test case #%d failed: 0 hits returned", testCaseNum) + } + if len(controlResult.Hits) != len(experimentalResult.Hits) { + t.Fatalf("test case #%d failed: expected %d results, got %d", testCaseNum, len(controlResult.Hits), len(experimentalResult.Hits)) + } + if controlResult.Total != experimentalResult.Total { + t.Fatalf("test case #%d failed: expected total hits to be %d, got %d", testCaseNum, controlResult.Total, experimentalResult.Total) + } + // KNN Metadata -> Score Breakdown and IndexNames MUST be omitted from the final hits + if !finalHitsOmitKNNMetadata(controlResult.Hits) || !finalHitsOmitKNNMetadata(experimentalResult.Hits) { + t.Fatalf("test case #%d failed: expected no KNN metadata in hits", testCaseNum) + } + if controlResult.Took == 0 || experimentalResult.Took == 0 { + t.Fatalf("test case #%d failed: expected non-zero took time", testCaseNum) + } + if controlResult.Request == nil || experimentalResult.Request == nil { + t.Fatalf("test case #%d failed: expected non-nil request", testCaseNum) + } + if verifyOnlyDocIDs { + // in multi partitioned index, we cannot be sure of the score or the ordering of the hits as the tf-idf scores are localized to each partition + // so we only check the ids + controlMap := make(map[string]struct{}) + experimentalMap := make(map[string]struct{}) + for _, hit := range controlResult.Hits { + controlMap[hit.ID] = struct{}{} + } + for _, hit := range experimentalResult.Hits { + experimentalMap[hit.ID] = struct{}{} + } + if len(controlMap) != len(experimentalMap) { + t.Fatalf("test case #%d failed: expected %d results, got %d", testCaseNum, len(controlMap), len(experimentalMap)) + } + for id := range controlMap { + if _, ok := experimentalMap[id]; !ok { + t.Fatalf("test case #%d failed: expected id %s to be in experimental result", testCaseNum, id) + } + } + return + } + for i := 0; i < len(controlResult.Hits); i++ { + if controlResult.Hits[i].ID != experimentalResult.Hits[i].ID { + t.Fatalf("test case #%d failed: expected hit %d to have id %s, got %s", testCaseNum, i, controlResult.Hits[i].ID, experimentalResult.Hits[i].ID) + } + // Truncate to 6 decimal places + actualScore := truncateScore(experimentalResult.Hits[i].Score) + expectScore := truncateScore(controlResult.Hits[i].Score) + if expectScore != actualScore { + t.Fatalf("test case #%d failed: expected hit %d to have score %f, got %f", testCaseNum, i, expectScore, actualScore) + } + if !compareExplanation(controlResult.Hits[i].Expl, experimentalResult.Hits[i].Expl) { + t.Fatalf("test case #%d failed: expected hit %d to have explanation %v, got %v", testCaseNum, i, controlResult.Hits[i].Expl, experimentalResult.Hits[i].Expl) + } + } + if truncateScore(controlResult.MaxScore) != truncateScore(experimentalResult.MaxScore) { + t.Fatalf("test case #%d: expected maxScore to be %f, got %f", testCaseNum, controlResult.MaxScore, experimentalResult.MaxScore) + } +} + +func TestSimilaritySearchMultipleSegments(t *testing.T) { + // using scorch options to prevent merges during the course of this test + // so that the knnCollector can be accurately tested + scorch.DefaultMemoryPressurePauseThreshold = 0 + scorch.DefaultMinSegmentsForInMemoryMerge = math.MaxInt + dataset, searchRequests, err := readDatasetAndQueries(testInputCompressedFile) + if err != nil { + t.Fatal(err) + } + documents := makeDatasetIntoDocuments(dataset) + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Analyzer = en.AnalyzerName + + vecFieldMappingL2 := mapping.NewVectorFieldMapping() + vecFieldMappingL2.Dims = testDatasetDims + vecFieldMappingL2.Similarity = index.EuclideanDistance + + vecFieldMappingDot := mapping.NewVectorFieldMapping() + vecFieldMappingDot.Dims = testDatasetDims + vecFieldMappingDot.Similarity = index.InnerProduct + + vecFieldMappingCosine := mapping.NewVectorFieldMapping() + vecFieldMappingCosine.Dims = testDatasetDims + vecFieldMappingCosine.Similarity = index.CosineSimilarity + + indexMappingL2Norm := NewIndexMapping() + indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping) + indexMappingL2Norm.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingL2) + + indexMappingDotProduct := NewIndexMapping() + indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping) + indexMappingDotProduct.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingDot) + + indexMappingCosine := NewIndexMapping() + indexMappingCosine.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping) + indexMappingCosine.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMappingCosine) + + var reqSort = search.SortOrder{&search.SortScore{Desc: true}, &search.SortDocID{Desc: true}, &search.SortField{Desc: false, Field: "content"}} + + testCases := []struct { + numSegments int + queryIndex int + mapping mapping.IndexMapping + scoreValue string + }{ + // L2 norm similarity + { + numSegments: 6, + queryIndex: 0, + mapping: indexMappingL2Norm, + }, + { + numSegments: 7, + queryIndex: 1, + mapping: indexMappingL2Norm, + }, + { + numSegments: 8, + queryIndex: 2, + mapping: indexMappingL2Norm, + }, + { + numSegments: 9, + queryIndex: 3, + mapping: indexMappingL2Norm, + }, + { + numSegments: 10, + queryIndex: 4, + mapping: indexMappingL2Norm, + }, + { + numSegments: 11, + queryIndex: 5, + mapping: indexMappingL2Norm, + }, + // dot_product similarity + { + numSegments: 6, + queryIndex: 0, + mapping: indexMappingDotProduct, + }, + { + numSegments: 7, + queryIndex: 1, + mapping: indexMappingDotProduct, + }, + { + numSegments: 8, + queryIndex: 2, + mapping: indexMappingDotProduct, + }, + { + numSegments: 9, + queryIndex: 3, + mapping: indexMappingDotProduct, + }, + { + numSegments: 10, + queryIndex: 4, + mapping: indexMappingDotProduct, + }, + { + numSegments: 11, + queryIndex: 5, + mapping: indexMappingDotProduct, + }, + // cosine similarity + { + numSegments: 9, + queryIndex: 0, + mapping: indexMappingCosine, + }, + { + numSegments: 5, + queryIndex: 1, + mapping: indexMappingCosine, + }, + { + numSegments: 4, + queryIndex: 2, + mapping: indexMappingCosine, + }, + { + numSegments: 12, + queryIndex: 3, + mapping: indexMappingCosine, + }, + { + numSegments: 7, + queryIndex: 4, + mapping: indexMappingCosine, + }, + { + numSegments: 11, + queryIndex: 5, + mapping: indexMappingCosine, + }, + // score none test + { + numSegments: 3, + queryIndex: 0, + mapping: indexMappingL2Norm, + scoreValue: "none", + }, + { + numSegments: 7, + queryIndex: 1, + mapping: indexMappingL2Norm, + scoreValue: "none", + }, + { + numSegments: 8, + queryIndex: 2, + mapping: indexMappingL2Norm, + scoreValue: "none", + }, + { + numSegments: 3, + queryIndex: 0, + mapping: indexMappingDotProduct, + scoreValue: "none", + }, + { + numSegments: 7, + queryIndex: 1, + mapping: indexMappingDotProduct, + scoreValue: "none", + }, + { + numSegments: 8, + queryIndex: 2, + mapping: indexMappingDotProduct, + scoreValue: "none", + }, + { + numSegments: 3, + queryIndex: 0, + mapping: indexMappingCosine, + scoreValue: "none", + }, + { + numSegments: 7, + queryIndex: 1, + mapping: indexMappingCosine, + scoreValue: "none", + }, + { + numSegments: 8, + queryIndex: 2, + mapping: indexMappingCosine, + scoreValue: "none", + }, + } + for testCaseNum, testCase := range testCases { + originalRequest := searchRequests[testCase.queryIndex] + for _, operator := range knnOperators { + // run single segment test first + tmpIndexPath := createTmpIndexPath(t) + index, err := New(tmpIndexPath, testCase.mapping) + if err != nil { + t.Fatal(err) + } + query := copySearchRequest(originalRequest, nil) + query.Sort = reqSort.Copy() + query.AddKNNOperator(operator) + query.Explain = true + + nameToIndex := make(map[string]Index) + nameToIndex[index.Name()] = index + + err = createMultipleSegmentsIndex(documents, index, 1) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + controlResult, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(controlResult.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected control result hits to have valid `Index`", testCaseNum) + } + if testCase.scoreValue == "none" { + + query := copySearchRequest(originalRequest, nil) + query.Sort = reqSort.Copy() + query.AddKNNOperator(operator) + query.Explain = true + query.Score = testCase.scoreValue + + expectedResultScoreNone, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(expectedResultScoreNone.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected score none hits to have valid `Index`", testCaseNum) + } + verifyResult(t, controlResult, expectedResultScoreNone, testCaseNum, true) + } + cleanUp(t, nameToIndex) + + // run multiple segments test + tmpIndexPath = createTmpIndexPath(t) + index, err = New(tmpIndexPath, testCase.mapping) + if err != nil { + t.Fatal(err) + } + nameToIndex = make(map[string]Index) + nameToIndex[index.Name()] = index + err = createMultipleSegmentsIndex(documents, index, testCase.numSegments) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + + query = copySearchRequest(originalRequest, nil) + query.Sort = reqSort.Copy() + query.AddKNNOperator(operator) + query.Explain = true + + experimentalResult, err := index.Search(query) + if err != nil { + cleanUp(t, nameToIndex) + t.Fatal(err) + } + if !finalHitsHaveValidIndex(experimentalResult.Hits, nameToIndex) { + cleanUp(t, nameToIndex) + t.Fatalf("test case #%d failed: expected experimental result hits to have valid `Index`", testCaseNum) + } + verifyResult(t, controlResult, experimentalResult, testCaseNum, false) + cleanUp(t, nameToIndex) + } + } +} + +// Test to determine the impact of boost on kNN queries. +func TestKNNScoreBoosting(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + const dims = 5 + getRandomVector := func() []float32 { + vec := make([]float32, dims) + for i := 0; i < dims; i++ { + vec[i] = rand.Float32() + } + return vec + } + + dataset := make([]map[string]interface{}, 10) + + // Indexing just a few docs to populate index. + for i := 0; i < 100; i++ { + dataset = append(dataset, map[string]interface{}{ + "type": "vectorStuff", + "content": strconv.Itoa(i), + "vector": getRandomVector(), + }) + } + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("vectorStuff", documentMapping) + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Index = true + contentFieldMapping.Store = true + documentMapping.AddFieldMappingsAt("content", contentFieldMapping) + + vecFieldMapping := mapping.NewVectorFieldMapping() + vecFieldMapping.Index = true + vecFieldMapping.Dims = 5 + vecFieldMapping.Similarity = "dot_product" + documentMapping.AddFieldMappingsAt("vector", vecFieldMapping) + + index, err := New(tmpIndexPath, indexMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := index.NewBatch() + for i := 0; i < len(dataset); i++ { + err = batch.Index(strconv.Itoa(i), dataset[i]) + if err != nil { + t.Fatal(err) + } + } + + err = index.Batch(batch) + if err != nil { + t.Fatal(err) + } + + queryVec := getRandomVector() + searchRequest := NewSearchRequest(NewMatchNoneQuery()) + searchRequest.AddKNN("vector", queryVec, 3, 1.0) + searchRequest.Fields = []string{"content", "vector"} + + hits, _ := index.Search(searchRequest) + hitsMap := make(map[string]float64, 0) + for _, hit := range hits.Hits { + hitsMap[hit.ID] = (hit.Score) + } + + searchRequest2 := NewSearchRequest(NewMatchNoneQuery()) + searchRequest.AddKNN("vector", queryVec, 3, 10.0) + searchRequest.Fields = []string{"content", "vector"} + + hits2, _ := index.Search(searchRequest2) + hitsMap2 := make(map[string]float64, 0) + for _, hit := range hits2.Hits { + hitsMap2[hit.ID] = (hit.Score) + } + + for _, hit := range hits2.Hits { + if hitsMap[hit.ID] != hitsMap2[hit.ID]/10 { + t.Errorf("boosting not working: %v %v \n", hitsMap[hit.ID], hitsMap2[hit.ID]) + } + } +} + +// Test to see if KNN Operators get added right to the query. +func TestKNNOperator(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + const dims = 5 + getRandomVector := func() []float32 { + vec := make([]float32, dims) + for i := 0; i < dims; i++ { + vec[i] = rand.Float32() + } + return vec + } + + dataset := make([]map[string]interface{}, 10) + + // Indexing just a few docs to populate index. + for i := 0; i < 10; i++ { + dataset = append(dataset, map[string]interface{}{ + "type": "vectorStuff", + "content": strconv.Itoa(i), + "vector": getRandomVector(), + }) + } + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("vectorStuff", documentMapping) + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Index = true + contentFieldMapping.Store = true + documentMapping.AddFieldMappingsAt("content", contentFieldMapping) + + vecFieldMapping := mapping.NewVectorFieldMapping() + vecFieldMapping.Index = true + vecFieldMapping.Dims = 5 + vecFieldMapping.Similarity = "dot_product" + documentMapping.AddFieldMappingsAt("vector", vecFieldMapping) + + index, err := New(tmpIndexPath, indexMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := index.NewBatch() + for i := 0; i < len(dataset); i++ { + err = batch.Index(strconv.Itoa(i), dataset[i]) + if err != nil { + t.Fatal(err) + } + } + + err = index.Batch(batch) + if err != nil { + t.Fatal(err) + } + + termQuery := query.NewTermQuery("2") + + searchRequest := NewSearchRequest(termQuery) + searchRequest.AddKNN("vector", getRandomVector(), 3, 2.0) + searchRequest.AddKNN("vector", getRandomVector(), 2, 1.5) + searchRequest.Fields = []string{"content", "vector"} + + // Conjunction + searchRequest.AddKNNOperator(knnOperatorAnd) + conjunction, _, _, err := createKNNQuery(searchRequest, nil) + if err != nil { + t.Fatalf("unexpected error for AND knn operator") + } + + conj, ok := conjunction.(*query.DisjunctionQuery) + if !ok { + t.Fatalf("expected disjunction query") + } + + if len(conj.Disjuncts) != 2 { + t.Fatalf("expected 2 disjuncts") + } + + // Disjunction + searchRequest.AddKNNOperator(knnOperatorOr) + disjunction, _, _, err := createKNNQuery(searchRequest, nil) + if err != nil { + t.Fatalf("unexpected error for OR knn operator") + } + + disj, ok := disjunction.(*query.DisjunctionQuery) + if !ok { + t.Fatalf("expected disjunction query") + } + + if len(disj.Disjuncts) != 2 { + t.Fatalf("expected 2 disjuncts") + } + + // Incorrect operator. + searchRequest.AddKNNOperator("bs_op") + searchRequest.Query, _, _, err = createKNNQuery(searchRequest, nil) + if err == nil { + t.Fatalf("expected error for incorrect knn operator") + } +} + +func TestKNNFiltering(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + const dims = 5 + getRandomVector := func() []float32 { + vec := make([]float32, dims) + for i := 0; i < dims; i++ { + vec[i] = rand.Float32() + } + return vec + } + + dataset := make([]map[string]interface{}, 0) + + // Indexing just a few docs to populate index. + for i := 0; i < 10; i++ { + dataset = append(dataset, map[string]interface{}{ + "type": "vectorStuff", + "content": strconv.Itoa(i + 1000), + "vector": getRandomVector(), + }) + } + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("vectorStuff", documentMapping) + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Index = true + contentFieldMapping.Store = true + documentMapping.AddFieldMappingsAt("content", contentFieldMapping) + + vecFieldMapping := mapping.NewVectorFieldMapping() + vecFieldMapping.Index = true + vecFieldMapping.Dims = 5 + vecFieldMapping.Similarity = "dot_product" + documentMapping.AddFieldMappingsAt("vector", vecFieldMapping) + + index, err := New(tmpIndexPath, indexMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := index.NewBatch() + for i := 0; i < len(dataset); i++ { + // the id of term "i" is (i-1000) + err = batch.Index(strconv.Itoa(i), dataset[i]) + if err != nil { + t.Fatal(err) + } + } + + err = index.Batch(batch) + if err != nil { + t.Fatal(err) + } + + termQuery := query.NewTermQuery("1004") + filterRequest := NewSearchRequest(termQuery) + filteredHits, err := index.Search(filterRequest) + if err != nil { + t.Fatal(err) + } + filteredDocIDs := make(map[string]struct{}) + for _, match := range filteredHits.Hits { + filteredDocIDs[match.ID] = struct{}{} + } + + searchRequest := NewSearchRequest(NewMatchNoneQuery()) + searchRequest.AddKNNWithFilter("vector", getRandomVector(), 3, 2.0, termQuery) + searchRequest.Fields = []string{"content", "vector"} + + res, err := index.Search(searchRequest) + if err != nil { + t.Fatal(err) + } + // check if any of the returned results are not part of the filtered hits. + for _, match := range res.Hits { + if _, exists := filteredDocIDs[match.ID]; !exists { + t.Errorf("returned result not present in filtered hits") + } + } + + // No results should be returned with a match_none filter. + searchRequest = NewSearchRequest(NewMatchNoneQuery()) + searchRequest.AddKNNWithFilter("vector", getRandomVector(), 3, 2.0, + NewMatchNoneQuery()) + res, err = index.Search(searchRequest) + if err != nil { + t.Fatal(err) + } + if len(res.Hits) != 0 { + t.Errorf("match none filter should return no hits") + } + + // Testing with a disjunction query. + + termQuery = query.NewTermQuery("1003") + termQuery2 := query.NewTermQuery("1005") + disjQuery := query.NewDisjunctionQuery([]query.Query{termQuery, termQuery2}) + filterRequest = NewSearchRequest(disjQuery) + filteredHits, err = index.Search(filterRequest) + if err != nil { + t.Fatal(err) + } + filteredDocIDs = make(map[string]struct{}) + for _, match := range filteredHits.Hits { + filteredDocIDs[match.ID] = struct{}{} + } + + searchRequest = NewSearchRequest(NewMatchNoneQuery()) + searchRequest.AddKNNWithFilter("vector", getRandomVector(), 3, 2.0, disjQuery) + searchRequest.Fields = []string{"content", "vector"} + + res, err = index.Search(searchRequest) + if err != nil { + t.Fatal(err) + } + + for _, match := range res.Hits { + if _, exists := filteredDocIDs[match.ID]; !exists { + t.Errorf("returned result not present in filtered hits") + } + } +} + +// ----------------------------------------------------------------------------- +// Test nested vectors + +func TestNestedVectors(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + const dims = 3 + const k = 1 // one nearest neighbor + const vecFieldName = "vecData" + + dataset := map[string]map[string]interface{}{ // docID -> Doc + "doc1": { + vecFieldName: []float32{100, 100, 100}, + }, + "doc2": { + vecFieldName: [][]float32{{0, 0, 0}, {1000, 1000, 1000}}, + }, + } + + // Index mapping + indexMapping := NewIndexMapping() + vm := mapping.NewVectorFieldMapping() + vm.Dims = dims + vm.Similarity = "l2_norm" + indexMapping.DefaultMapping.AddFieldMappingsAt(vecFieldName, vm) + + // Create index and upload documents + index, err := New(tmpIndexPath, indexMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := index.NewBatch() + for docID, doc := range dataset { + err = batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + + err = index.Batch(batch) + if err != nil { + t.Fatal(err) + } + + // Run searches + + tests := []struct { + queryVec []float32 + expectedDocID string + }{ + { + queryVec: []float32{100, 100, 100}, + expectedDocID: "doc1", + }, + { + queryVec: []float32{0, 0, 0}, + expectedDocID: "doc2", + }, + { + queryVec: []float32{1000, 1000, 1000}, + expectedDocID: "doc2", + }, + } + + for _, test := range tests { + searchReq := NewSearchRequest(query.NewMatchNoneQuery()) + searchReq.AddKNNWithFilter(vecFieldName, test.queryVec, k, 1000, + NewMatchAllQuery()) + + res, err := index.Search(searchReq) + if err != nil { + t.Fatal(err) + } + + if len(res.Hits) != 1 { + t.Fatalf("expected 1 hit, got %d", len(res.Hits)) + } + + if res.Hits[0].ID != test.expectedDocID { + t.Fatalf("expected docID %s, got %s", test.expectedDocID, + res.Hits[0].ID) + } + } +} + +func TestNumVecsStat(t *testing.T) { + + dataset, _, err := readDatasetAndQueries(testInputCompressedFile) + if err != nil { + t.Fatal(err) + } + documents := makeDatasetIntoDocuments(dataset) + + indexMapping := NewIndexMapping() + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Analyzer = en.AnalyzerName + indexMapping.DefaultMapping.AddFieldMappingsAt("content", contentFieldMapping) + + vecFieldMapping1 := mapping.NewVectorFieldMapping() + vecFieldMapping1.Dims = testDatasetDims + vecFieldMapping1.Similarity = index.EuclideanDistance + indexMapping.DefaultMapping.AddFieldMappingsAt("vector", vecFieldMapping1) + + tmpIndexPath := createTmpIndexPath(t) + index, err := New(tmpIndexPath, indexMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + for i := 0; i < 10; i++ { + batch := index.NewBatch() + for j := 0; j < 3; j++ { + for k := 0; k < 10; k++ { + err := batch.Index(fmt.Sprintf("%d", i*30+j*10+k), documents[j*10+k]) + if err != nil { + t.Fatal(err) + } + } + } + err = index.Batch(batch) + if err != nil { + t.Fatal(err) + } + } + + statsMap := index.StatsMap() + + if indexStats, exists := statsMap["index"]; exists { + if indexStatsMap, ok := indexStats.(map[string]interface{}); ok { + v1, ok := indexStatsMap["field:vector:num_vectors"].(uint64) + if !ok || v1 != uint64(300) { + t.Fatalf("mismatch in the number of vectors, expected 300, got %d", indexStatsMap["field:vector:num_vectors"]) + } + } + } +} diff --git a/search_no_knn.go b/search_no_knn.go new file mode 100644 index 0000000..c919805 --- /dev/null +++ b/search_no_knn.go @@ -0,0 +1,209 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !vectors +// +build !vectors + +package bleve + +import ( + "context" + "encoding/json" + "sort" + + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/collector" + "github.com/blevesearch/bleve/v2/search/query" + index "github.com/blevesearch/bleve_index_api" +) + +const supportForVectorSearch = false + +// A SearchRequest describes all the parameters +// needed to search the index. +// Query is required. +// Size/From describe how much and which part of the +// result set to return. +// Highlight describes optional search result +// highlighting. +// Fields describes a list of field values which +// should be retrieved for result documents, provided they +// were stored while indexing. +// Facets describe the set of facets to be computed. +// Explain triggers inclusion of additional search +// result score explanations. +// Sort describes the desired order for the results to be returned. +// Score controls the kind of scoring performed +// SearchAfter supports deep paging by providing a minimum sort key +// SearchBefore supports deep paging by providing a maximum sort key +// sortFunc specifies the sort implementation to use for sorting results. +// +// A special field named "*" can be used to return all fields. +type SearchRequest struct { + ClientContextID string `json:"client_context_id,omitempty"` + Query query.Query `json:"query"` + Size int `json:"size"` + From int `json:"from"` + Highlight *HighlightRequest `json:"highlight"` + Fields []string `json:"fields"` + Facets FacetsRequest `json:"facets"` + Explain bool `json:"explain"` + Sort search.SortOrder `json:"sort"` + IncludeLocations bool `json:"includeLocations"` + Score string `json:"score,omitempty"` + SearchAfter []string `json:"search_after"` + SearchBefore []string `json:"search_before"` + + // PreSearchData will be a map that will be used + // in the second phase of any 2-phase search, to provide additional + // context to the second phase. This is useful in the case of index + // aliases where the first phase will gather the PreSearchData from all + // the indexes in the alias, and the second phase will use that + // PreSearchData to perform the actual search. + // The currently accepted map configuration is: + // + // "_knn_pre_search_data_key": []*search.DocumentMatch + + PreSearchData map[string]interface{} `json:"pre_search_data,omitempty"` + + sortFunc func(sort.Interface) +} + +// UnmarshalJSON deserializes a JSON representation of +// a SearchRequest +func (r *SearchRequest) UnmarshalJSON(input []byte) error { + var temp struct { + Q json.RawMessage `json:"query"` + Size *int `json:"size"` + From int `json:"from"` + Highlight *HighlightRequest `json:"highlight"` + Fields []string `json:"fields"` + Facets FacetsRequest `json:"facets"` + Explain bool `json:"explain"` + Sort []json.RawMessage `json:"sort"` + IncludeLocations bool `json:"includeLocations"` + Score string `json:"score"` + SearchAfter []string `json:"search_after"` + SearchBefore []string `json:"search_before"` + PreSearchData json.RawMessage `json:"pre_search_data"` + } + + err := json.Unmarshal(input, &temp) + if err != nil { + return err + } + + if temp.Size == nil { + r.Size = 10 + } else { + r.Size = *temp.Size + } + if temp.Sort == nil { + r.Sort = search.SortOrder{&search.SortScore{Desc: true}} + } else { + r.Sort, err = search.ParseSortOrderJSON(temp.Sort) + if err != nil { + return err + } + } + r.From = temp.From + r.Explain = temp.Explain + r.Highlight = temp.Highlight + r.Fields = temp.Fields + r.Facets = temp.Facets + r.IncludeLocations = temp.IncludeLocations + r.Score = temp.Score + r.SearchAfter = temp.SearchAfter + r.SearchBefore = temp.SearchBefore + r.Query, err = query.ParseQuery(temp.Q) + if err != nil { + return err + } + + if r.Size < 0 { + r.Size = 10 + } + if r.From < 0 { + r.From = 0 + } + if temp.PreSearchData != nil { + r.PreSearchData, err = query.ParsePreSearchData(temp.PreSearchData) + if err != nil { + return err + } + } + + return nil + +} + +// ----------------------------------------------------------------------------- + +func copySearchRequest(req *SearchRequest, preSearchData map[string]interface{}) *SearchRequest { + rv := SearchRequest{ + Query: req.Query, + Size: req.Size + req.From, + From: 0, + Highlight: req.Highlight, + Fields: req.Fields, + Facets: req.Facets, + Explain: req.Explain, + Sort: req.Sort.Copy(), + IncludeLocations: req.IncludeLocations, + Score: req.Score, + SearchAfter: req.SearchAfter, + SearchBefore: req.SearchBefore, + PreSearchData: preSearchData, + } + return &rv +} + +func validateKNN(req *SearchRequest) error { + return nil +} + +func (i *indexImpl) runKnnCollector(ctx context.Context, req *SearchRequest, reader index.IndexReader, preSearch bool) ([]*search.DocumentMatch, error) { + return nil, nil +} + +func setKnnHitsInCollector(knnHits []*search.DocumentMatch, req *SearchRequest, coll *collector.TopNCollector) { +} + +func requestHasKNN(req *SearchRequest) bool { + return false +} + +func addKnnToDummyRequest(dummyReq *SearchRequest, realReq *SearchRequest) { +} + +func validateAndDistributeKNNHits(knnHits []*search.DocumentMatch, indexes []Index) (map[string][]*search.DocumentMatch, error) { + return nil, nil +} + +func isKNNrequestSatisfiedByPreSearch(req *SearchRequest) bool { + return false +} + +func constructKnnPreSearchData(mergedOut map[string]map[string]interface{}, preSearchResult *SearchResult, + indexes []Index) (map[string]map[string]interface{}, error) { + return mergedOut, nil +} + +func finalizeKNNResults(req *SearchRequest, knnHits []*search.DocumentMatch) []*search.DocumentMatch { + return knnHits +} + +func newKnnPreSearchResultProcessor(req *SearchRequest) *knnPreSearchResultProcessor { + return &knnPreSearchResultProcessor{} // equivalent to nil +} diff --git a/search_test.go b/search_test.go new file mode 100644 index 0000000..a69d138 --- /dev/null +++ b/search_test.go @@ -0,0 +1,4544 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bleve + +import ( + "context" + "encoding/json" + "fmt" + "math" + "math/rand" + "reflect" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/blevesearch/bleve/v2/analysis" + "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" + "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" + "github.com/blevesearch/bleve/v2/analysis/analyzer/simple" + "github.com/blevesearch/bleve/v2/analysis/analyzer/standard" + html_char_filter "github.com/blevesearch/bleve/v2/analysis/char/html" + regexp_char_filter "github.com/blevesearch/bleve/v2/analysis/char/regexp" + "github.com/blevesearch/bleve/v2/analysis/datetime/flexible" + "github.com/blevesearch/bleve/v2/analysis/datetime/iso" + "github.com/blevesearch/bleve/v2/analysis/datetime/percent" + "github.com/blevesearch/bleve/v2/analysis/datetime/sanitized" + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/microseconds" + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/milliseconds" + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/nanoseconds" + "github.com/blevesearch/bleve/v2/analysis/datetime/timestamp/seconds" + "github.com/blevesearch/bleve/v2/analysis/lang/en" + "github.com/blevesearch/bleve/v2/analysis/token/length" + "github.com/blevesearch/bleve/v2/analysis/token/lowercase" + "github.com/blevesearch/bleve/v2/analysis/token/shingle" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/single" + "github.com/blevesearch/bleve/v2/analysis/tokenizer/whitespace" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/geo" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + "github.com/blevesearch/bleve/v2/search/highlight/highlighter/ansi" + "github.com/blevesearch/bleve/v2/search/highlight/highlighter/html" + "github.com/blevesearch/bleve/v2/search/query" + index "github.com/blevesearch/bleve_index_api" +) + +func TestSortedFacetedQuery(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + indexMapping.AddDocumentMapping("hotel", documentMapping) + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Index = true + contentFieldMapping.DocValues = true + documentMapping.AddFieldMappingsAt("content", contentFieldMapping) + documentMapping.AddFieldMappingsAt("country", contentFieldMapping) + + index, err := New(tmpIndexPath, indexMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + if err := index.Index("1", map[string]interface{}{ + "country": "india", + "content": "k", + }); err != nil { + t.Fatal(err) + } + + if err := index.Index("2", map[string]interface{}{ + "country": "india", + "content": "l", + }); err != nil { + t.Fatal(err) + } + + if err := index.Index("3", map[string]interface{}{ + "country": "india", + "content": "k", + }); err != nil { + t.Fatal(err) + } + + d, err := index.DocCount() + if err != nil { + t.Fatal(err) + } + + if d != 3 { + t.Errorf("expected 3, got %d", d) + } + + query := NewMatchPhraseQuery("india") + query.SetField("country") + searchRequest := NewSearchRequest(query) + searchRequest.SortBy([]string{"content"}) + fr := NewFacetRequest("content", 100) + searchRequest.AddFacet("content_facet", fr) + + searchResults, err := index.Search(searchRequest) + if err != nil { + t.Fatal(err) + } + + expectedResults := map[string]int{"k": 2, "l": 1} + + for _, v := range searchResults.Facets { + for _, v1 := range v.Terms.Terms() { + if v1.Count != expectedResults[v1.Term] { + t.Errorf("expected %d, got %d", expectedResults[v1.Term], v1.Count) + } + } + } +} + +func TestMatchAllScorer(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + indexMapping := NewIndexMapping() + indexMapping.TypeField = "type" + indexMapping.DefaultAnalyzer = "en" + documentMapping := NewDocumentMapping() + + contentFieldMapping := NewTextFieldMapping() + contentFieldMapping.Index = true + contentFieldMapping.Store = true + documentMapping.AddFieldMappingsAt("content", contentFieldMapping) + + index, err := New(tmpIndexPath, indexMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err := index.Close() + if err != nil { + t.Fatal(err) + } + }() + + if err := index.Index("1", map[string]interface{}{ + "country": "india", + "content": "k", + }); err != nil { + t.Fatal(err) + } + + if err := index.Index("2", map[string]interface{}{ + "country": "india", + "content": "l", + }); err != nil { + t.Fatal(err) + } + + if err := index.Index("3", map[string]interface{}{ + "country": "india", + "content": "k", + }); err != nil { + t.Fatal(err) + } + + d, err := index.DocCount() + if err != nil { + t.Fatal(err) + } + if d != 3 { + t.Errorf("expected 3, got %d", d) + } + + searchRequest := NewSearchRequest(NewMatchAllQuery()) + searchRequest.Score = "none" + searchResults, err := index.Search(searchRequest) + if err != nil { + t.Fatal(err) + } + + if searchResults.Total != 3 { + t.Fatalf("expected all the 3 docs in the index, got %v", searchResults.Total) + } + + for _, hit := range searchResults.Hits { + if hit.Score != 0.0 { + t.Fatalf("expected 0 score since score = none, got %v", hit.Score) + } + } +} + +func TestSearchResultString(t *testing.T) { + tests := []struct { + result *SearchResult + str string + }{ + { + result: &SearchResult{ + Request: &SearchRequest{ + Size: 10, + }, + Total: 5, + Took: 1 * time.Second, + Hits: search.DocumentMatchCollection{ + &search.DocumentMatch{}, + &search.DocumentMatch{}, + &search.DocumentMatch{}, + &search.DocumentMatch{}, + &search.DocumentMatch{}, + }, + }, + str: "5 matches, showing 1 through 5, took 1s", + }, + { + result: &SearchResult{ + Request: &SearchRequest{ + Size: 0, + }, + Total: 5, + Hits: search.DocumentMatchCollection{}, + }, + str: "5 matches", + }, + { + result: &SearchResult{ + Request: &SearchRequest{ + Size: 10, + }, + Total: 0, + Hits: search.DocumentMatchCollection{}, + }, + str: "No matches", + }, + } + + for _, test := range tests { + srstring := test.result.String() + if !strings.HasPrefix(srstring, test.str) { + t.Errorf("expected to start %s, got %s", test.str, srstring) + } + } +} + +func TestSearchResultMerge(t *testing.T) { + l := &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + MaxScore: 1, + Hits: search.DocumentMatchCollection{ + &search.DocumentMatch{ + ID: "a", + Score: 1, + }, + }, + } + + r := &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + MaxScore: 2, + Hits: search.DocumentMatchCollection{ + &search.DocumentMatch{ + ID: "b", + Score: 2, + }, + }, + } + + expected := &SearchResult{ + Status: &SearchStatus{ + Total: 2, + Successful: 2, + Errors: make(map[string]error), + }, + Total: 2, + MaxScore: 2, + Hits: search.DocumentMatchCollection{ + &search.DocumentMatch{ + ID: "a", + Score: 1, + }, + &search.DocumentMatch{ + ID: "b", + Score: 2, + }, + }, + } + + l.Merge(r) + + if !reflect.DeepEqual(l, expected) { + t.Errorf("expected %#v, got %#v", expected, l) + } +} + +func TestUnmarshalingSearchResult(t *testing.T) { + searchResponse := []byte(`{ + "status":{ + "total":1, + "failed":1, + "successful":0, + "errors":{ + "default_index_362ce020b3d62b13_348f5c3c":"context deadline exceeded" + } + }, + "request":{ + "query":{ + "match":"emp", + "field":"type", + "boost":1, + "prefix_length":0, + "fuzziness":0 + }, + "size":10000000, + "from":0, + "highlight":null, + "fields":[], + "facets":null, + "explain":false + }, + "hits":null, + "total_hits":0, + "max_score":0, + "took":0, + "facets":null +}`) + + rv := &SearchResult{ + Status: &SearchStatus{ + Errors: make(map[string]error), + }, + } + err = json.Unmarshal(searchResponse, rv) + if err != nil { + t.Error(err) + } + if len(rv.Status.Errors) != 1 { + t.Errorf("expected 1 error, got %d", len(rv.Status.Errors)) + } +} + +func TestFacetNumericDateRangeRequests(t *testing.T) { + drMissingErr := fmt.Errorf("date range query must specify either start, end or both for range name 'testName'") + nrMissingErr := fmt.Errorf("numeric range query must specify either min, max or both for range name 'testName'") + drNrErr := fmt.Errorf("facet can only contain numeric ranges or date ranges, not both") + drNameDupErr := fmt.Errorf("date ranges contains duplicate name 'testName'") + nrNameDupErr := fmt.Errorf("numeric ranges contains duplicate name 'testName'") + value := float64(5) + + tests := []struct { + facet *FacetRequest + result error + }{ + { + facet: &FacetRequest{ + Field: "Date_Range_Success_With_StartEnd", + Size: 1, + DateTimeRanges: []*dateTimeRange{ + {Name: "testName", Start: time.Unix(0, 0), End: time.Now()}, + }, + }, + result: nil, + }, + { + facet: &FacetRequest{ + Field: "Date_Range_Success_With_Start", + Size: 1, + DateTimeRanges: []*dateTimeRange{ + {Name: "testName", Start: time.Unix(0, 0)}, + }, + }, + result: nil, + }, + { + facet: &FacetRequest{ + Field: "Date_Range_Success_With_End", + Size: 1, + DateTimeRanges: []*dateTimeRange{ + {Name: "testName", End: time.Now()}, + }, + }, + result: nil, + }, + { + facet: &FacetRequest{ + Field: "Numeric_Range_Success_With_MinMax", + Size: 1, + NumericRanges: []*numericRange{ + {Name: "testName", Min: &value, Max: &value}, + }, + }, + result: nil, + }, + { + facet: &FacetRequest{ + Field: "Numeric_Range_Success_With_Min", + Size: 1, + NumericRanges: []*numericRange{ + {Name: "testName", Min: &value}, + }, + }, + result: nil, + }, + { + facet: &FacetRequest{ + Field: "Numeric_Range_Success_With_Max", + Size: 1, + NumericRanges: []*numericRange{ + {Name: "testName", Max: &value}, + }, + }, + result: nil, + }, + { + facet: &FacetRequest{ + Field: "Date_Range_Missing_Failure", + Size: 1, + DateTimeRanges: []*dateTimeRange{ + {Name: "testName2", Start: time.Unix(0, 0)}, + {Name: "testName1", End: time.Now()}, + {Name: "testName"}, + }, + }, + result: drMissingErr, + }, + { + facet: &FacetRequest{ + Field: "Numeric_Range_Missing_Failure", + Size: 1, + NumericRanges: []*numericRange{ + {Name: "testName2", Min: &value}, + {Name: "testName1", Max: &value}, + {Name: "testName"}, + }, + }, + result: nrMissingErr, + }, + { + facet: &FacetRequest{ + Field: "Numeric_And_DateRanges_Failure", + Size: 1, + NumericRanges: []*numericRange{ + {Name: "testName", Max: &value}, + }, + DateTimeRanges: []*dateTimeRange{ + {Name: "testName", End: time.Now()}, + }, + }, + result: drNrErr, + }, + { + facet: &FacetRequest{ + Field: "Numeric_Range_Name_Repeat_Failure", + Size: 1, + NumericRanges: []*numericRange{ + {Name: "testName", Min: &value}, + {Name: "testName", Max: &value}, + }, + }, + result: nrNameDupErr, + }, + { + facet: &FacetRequest{ + Field: "Date_Range_Name_Repeat_Failure", + Size: 1, + DateTimeRanges: []*dateTimeRange{ + {Name: "testName", Start: time.Unix(0, 0)}, + {Name: "testName", End: time.Now()}, + }, + }, + result: drNameDupErr, + }, + } + + for _, test := range tests { + result := test.facet.Validate() + if !reflect.DeepEqual(result, test.result) { + t.Errorf("expected %#v, got %#v", test.result, result) + } + } +} + +func TestSearchResultFacetsMerge(t *testing.T) { + lowmed := "2010-01-01" + medhi := "2011-01-01" + hihigher := "2012-01-01" + + fr := &search.FacetResult{ + Field: "birthday", + Total: 100, + Missing: 25, + Other: 25, + DateRanges: []*search.DateRangeFacet{ + { + Name: "low", + End: &lowmed, + Count: 25, + }, + { + Name: "med", + Count: 24, + Start: &lowmed, + End: &medhi, + }, + { + Name: "hi", + Count: 1, + Start: &medhi, + End: &hihigher, + }, + }, + } + frs := search.FacetResults{ + "birthdays": fr, + } + + l := &SearchResult{ + Status: &SearchStatus{ + Total: 10, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 10, + MaxScore: 1, + } + + r := &SearchResult{ + Status: &SearchStatus{ + Total: 1, + Successful: 1, + Errors: make(map[string]error), + }, + Total: 1, + MaxScore: 2, + Facets: frs, + } + + expected := &SearchResult{ + Status: &SearchStatus{ + Total: 11, + Successful: 2, + Errors: make(map[string]error), + }, + Total: 11, + MaxScore: 2, + Facets: frs, + } + + l.Merge(r) + + if !reflect.DeepEqual(l, expected) { + t.Errorf("expected %#v, got %#v", expected, l) + } +} + +func TestMemoryNeededForSearchResult(t *testing.T) { + query := NewTermQuery("blah") + req := NewSearchRequest(query) + + var sr SearchResult + expect := sr.Size() + var dm search.DocumentMatch + expect += 10 * dm.Size() + + estimate := MemoryNeededForSearchResult(req) + if estimate != uint64(expect) { + t.Errorf("estimate not what is expected: %v != %v", estimate, expect) + } +} + +// https://github.com/blevesearch/bleve/issues/954 +func TestNestedBooleanSearchers(t *testing.T) { + // create an index with a custom analyzer + idxMapping := NewIndexMapping() + if err := idxMapping.AddCustomAnalyzer("3xbla", map[string]interface{}{ + "type": custom.Name, + "tokenizer": whitespace.Name, + "token_filters": []interface{}{lowercase.Name, "stop_en"}, + }); err != nil { + t.Fatal(err) + } + + idxMapping.DefaultAnalyzer = "3xbla" + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // create and insert documents as a batch + batch := idx.NewBatch() + matches := 0 + for i := 0; i < 100; i++ { + hostname := fmt.Sprintf("planner_hostname_%d", i%5) + metadata := map[string]string{"region": fmt.Sprintf("planner_us-east-%d", i%5)} + + // Expected matches + if (hostname == "planner_hostname_1" || hostname == "planner_hostname_2") && + metadata["region"] == "planner_us-east-1" { + matches++ + } + + doc := document.NewDocument(strconv.Itoa(i)) + doc.Fields = []document.Field{ + document.NewTextFieldCustom("hostname", []uint64{}, []byte(hostname), + index.IndexField, + &analysis.DefaultAnalyzer{ + Tokenizer: single.NewSingleTokenTokenizer(), + TokenFilters: []analysis.TokenFilter{ + lowercase.NewLowerCaseFilter(), + }, + }, + ), + } + for k, v := range metadata { + doc.AddField(document.NewTextFieldWithIndexingOptions( + fmt.Sprintf("metadata.%s", k), []uint64{}, []byte(v), index.IndexField)) + } + doc.CompositeFields = []*document.CompositeField{ + document.NewCompositeFieldWithIndexingOptions( + "_all", true, []string{"text"}, []string{}, + index.IndexField|index.IncludeTermVectors), + } + + if err = batch.IndexAdvanced(doc); err != nil { + t.Fatal(err) + } + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + que, err := query.ParseQuery([]byte( + `{ + "conjuncts": [ + { + "must": { + "conjuncts": [ + { + "disjuncts": [ + { + "match": "planner_hostname_1", + "field": "hostname" + }, + { + "match": "planner_hostname_2", + "field": "hostname" + } + ] + } + ] + } + }, + { + "must": { + "conjuncts": [ + { + "match": "planner_us-east-1", + "field": "metadata.region" + } + ] + } + } + ] + }`, + )) + if err != nil { + t.Fatal(err) + } + + req := NewSearchRequest(que) + req.Size = 100 + req.Fields = []string{"hostname", "metadata.region"} + searchResults, err := idx.Search(req) + if err != nil { + t.Fatal(err) + } + if matches != len(searchResults.Hits) { + t.Fatalf("Unexpected result set, %v != %v", matches, len(searchResults.Hits)) + } +} + +func TestNestedBooleanMustNotSearcherUpsidedown(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + // create an index with default settings + idxMapping := NewIndexMapping() + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // create and insert documents as a batch + batch := idx.NewBatch() + + docs := []struct { + id string + hasRole bool + investigationId string + }{ + { + id: "1@1", + hasRole: true, + investigationId: "1", + }, + { + id: "1@2", + hasRole: false, + investigationId: "2", + }, + { + id: "2@1", + hasRole: true, + investigationId: "1", + }, + { + id: "2@2", + hasRole: false, + investigationId: "2", + }, + { + id: "3@1", + hasRole: true, + investigationId: "1", + }, + { + id: "3@2", + hasRole: false, + investigationId: "2", + }, + { + id: "4@1", + hasRole: true, + investigationId: "1", + }, + { + id: "5@1", + hasRole: true, + investigationId: "1", + }, + { + id: "6@1", + hasRole: true, + investigationId: "1", + }, + { + id: "7@1", + hasRole: true, + investigationId: "1", + }, + } + + for i := 0; i < len(docs); i++ { + doc := document.NewDocument(docs[i].id) + doc.Fields = []document.Field{ + document.NewTextField("id", []uint64{}, []byte(docs[i].id)), + document.NewBooleanField("hasRole", []uint64{}, docs[i].hasRole), + document.NewTextField("investigationId", []uint64{}, []byte(docs[i].investigationId)), + } + + doc.CompositeFields = []*document.CompositeField{ + document.NewCompositeFieldWithIndexingOptions( + "_all", true, []string{"text"}, []string{}, + index.IndexField|index.IncludeTermVectors), + } + + if err = batch.IndexAdvanced(doc); err != nil { + t.Fatal(err) + } + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + tq := NewTermQuery("1") + tq.SetField("investigationId") + // using must not, for cases that the field did not exists at all + hasRole := NewBoolFieldQuery(true) + hasRole.SetField("hasRole") + noRole := NewBooleanQuery() + noRole.AddMustNot(hasRole) + oneRolesOrNoRoles := NewBooleanQuery() + oneRolesOrNoRoles.AddShould(noRole) + oneRolesOrNoRoles.SetMinShould(1) + q := NewConjunctionQuery(tq, oneRolesOrNoRoles) + + sr := NewSearchRequestOptions(q, 100, 0, false) + sr.Fields = []string{"hasRole"} + sr.Highlight = NewHighlight() + + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if res.Total != 0 { + t.Fatalf("Unexpected result, %v != 0", res.Total) + } +} + +func TestSearchScorchOverEmptyKeyword(t *testing.T) { + defaultIndexType := Config.DefaultIndexType + Config.DefaultIndexType = scorch.Name + + dmap := mapping.NewDocumentMapping() + dmap.DefaultAnalyzer = standard.Name + + fm := mapping.NewTextFieldMapping() + fm.Analyzer = keyword.Name + + fm1 := mapping.NewTextFieldMapping() + fm1.Analyzer = standard.Name + + dmap.AddFieldMappingsAt("id", fm) + dmap.AddFieldMappingsAt("name", fm1) + + imap := mapping.NewIndexMapping() + imap.DefaultMapping = dmap + imap.DefaultAnalyzer = standard.Name + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + Config.DefaultIndexType = defaultIndexType + }() + + for i := 0; i < 10; i++ { + err = idx.Index(fmt.Sprint(i), map[string]string{"name": fmt.Sprintf("test%d", i), "id": ""}) + if err != nil { + t.Fatal(err) + } + } + + count, err := idx.DocCount() + if err != nil { + t.Fatal(err) + } + if count != 10 { + t.Fatalf("Unexpected doc count: %v, expected 10", count) + } + + q := query.NewWildcardQuery("test*") + sr := NewSearchRequestOptions(q, 40, 0, false) + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if res.Total != 10 { + t.Fatalf("Unexpected search hits: %v, expected 10", res.Total) + } +} + +func TestMultipleNestedBooleanMustNotSearchersOnScorch(t *testing.T) { + defaultIndexType := Config.DefaultIndexType + Config.DefaultIndexType = scorch.Name + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + // create an index with default settings + idxMapping := NewIndexMapping() + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + + Config.DefaultIndexType = defaultIndexType + }() + + // create and insert documents as a batch + batch := idx.NewBatch() + + doc := document.NewDocument("1-child-0") + doc.Fields = []document.Field{ + document.NewTextField("id", []uint64{}, []byte("1-child-0")), + document.NewBooleanField("hasRole", []uint64{}, false), + document.NewTextField("roles", []uint64{}, []byte("R1")), + document.NewNumericField("type", []uint64{}, 0), + } + doc.CompositeFields = []*document.CompositeField{ + document.NewCompositeFieldWithIndexingOptions( + "_all", true, []string{"text"}, []string{}, + index.IndexField|index.IncludeTermVectors), + } + + if err = batch.IndexAdvanced(doc); err != nil { + t.Fatal(err) + } + + docs := []struct { + id string + hasRole bool + typ int + }{ + { + id: "16d6fa37-48fd-4dea-8b3d-a52bddf73951", + hasRole: false, + typ: 9, + }, + { + id: "18fa9eb2-8b1f-46f0-8b56-b4c551213f78", + hasRole: false, + typ: 9, + }, + { + id: "3085855b-d74b-474a-86c3-9bf3e4504382", + hasRole: false, + typ: 9, + }, + { + id: "38ef5d28-0f85-4fb0-8a94-dd20751c3364", + hasRole: false, + typ: 9, + }, + } + + for i := 0; i < len(docs); i++ { + doc := document.NewDocument(docs[i].id) + doc.Fields = []document.Field{ + document.NewTextField("id", []uint64{}, []byte(docs[i].id)), + document.NewBooleanField("hasRole", []uint64{}, docs[i].hasRole), + document.NewNumericField("type", []uint64{}, float64(docs[i].typ)), + } + + doc.CompositeFields = []*document.CompositeField{ + document.NewCompositeFieldWithIndexingOptions( + "_all", true, []string{"text"}, []string{}, + index.IndexField|index.IncludeTermVectors), + } + + if err = batch.IndexAdvanced(doc); err != nil { + t.Fatal(err) + } + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + batch = idx.NewBatch() + + // Update 1st doc + doc = document.NewDocument("1-child-0") + doc.Fields = []document.Field{ + document.NewTextField("id", []uint64{}, []byte("1-child-0")), + document.NewBooleanField("hasRole", []uint64{}, false), + document.NewNumericField("type", []uint64{}, 0), + } + doc.CompositeFields = []*document.CompositeField{ + document.NewCompositeFieldWithIndexingOptions( + "_all", true, []string{"text"}, []string{}, + index.IndexField|index.IncludeTermVectors), + } + + if err = batch.IndexAdvanced(doc); err != nil { + t.Fatal(err) + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + inclusive := true + val := float64(9) + q := query.NewNumericRangeInclusiveQuery(&val, &val, &inclusive, &inclusive) + q.SetField("type") + initialQuery := query.NewBooleanQuery(nil, nil, []query.Query{q}) + + // using must not, for cases that the field did not exists at all + hasRole := NewBoolFieldQuery(true) + hasRole.SetField("hasRole") + noRole := NewBooleanQuery() + noRole.AddMustNot(hasRole) + + rq := query.NewBooleanQuery([]query.Query{initialQuery, noRole}, nil, nil) + + sr := NewSearchRequestOptions(rq, 100, 0, false) + sr.Fields = []string{"id", "hasRole", "type"} + sr.Highlight = NewHighlight() + + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if res.Total != 1 { + t.Fatalf("Unexpected result, %v != 1", res.Total) + } +} + +func testBooleanMustNotSearcher(t *testing.T, indexName string) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + im := NewIndexMapping() + idx, err := NewUsing(tmpIndexPath, im, indexName, Config.DefaultKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + docs := []struct { + Name string + HasRole bool + }{ + { + Name: "13900", + }, + { + Name: "13901", + }, + { + Name: "13965", + }, + { + Name: "13966", + HasRole: true, + }, + { + Name: "13967", + HasRole: true, + }, + } + + for _, doc := range docs { + err := idx.Index(doc.Name, doc) + if err != nil { + t.Fatal(err) + } + } + + lhs := NewDocIDQuery([]string{"13965", "13966", "13967"}) + hasRole := NewBoolFieldQuery(true) + hasRole.SetField("HasRole") + rhs := NewBooleanQuery() + rhs.AddMustNot(hasRole) + + compareLeftRightAndConjunction := func(idx Index, left, right query.Query) error { + // left + lr := NewSearchRequestOptions(left, 100, 0, false) + lres, err := idx.Search(lr) + if err != nil { + return fmt.Errorf("error left: %v", err) + } + lresIds := map[string]struct{}{} + for i := range lres.Hits { + lresIds[lres.Hits[i].ID] = struct{}{} + } + // right + rr := NewSearchRequestOptions(right, 100, 0, false) + rres, err := idx.Search(rr) + if err != nil { + return fmt.Errorf("error right: %v", err) + } + rresIds := map[string]struct{}{} + for i := range rres.Hits { + rresIds[rres.Hits[i].ID] = struct{}{} + } + // conjunction + cr := NewSearchRequestOptions(NewConjunctionQuery(left, right), 100, 0, false) + cres, err := idx.Search(cr) + if err != nil { + return fmt.Errorf("error conjunction: %v", err) + } + for i := range cres.Hits { + if _, ok := lresIds[cres.Hits[i].ID]; ok { + if _, ok := rresIds[cres.Hits[i].ID]; !ok { + return fmt.Errorf("error id %s missing from right", cres.Hits[i].ID) + } + } else { + return fmt.Errorf("error id %s missing from left", cres.Hits[i].ID) + } + } + return nil + } + + err = compareLeftRightAndConjunction(idx, lhs, rhs) + if err != nil { + t.Fatal(err) + } +} + +func TestBooleanMustNotSearcherUpsidedown(t *testing.T) { + testBooleanMustNotSearcher(t, upsidedown.Name) +} + +func TestBooleanMustNotSearcherScorch(t *testing.T) { + testBooleanMustNotSearcher(t, scorch.Name) +} + +func TestQueryStringEmptyConjunctionSearcher(t *testing.T) { + mapping := NewIndexMapping() + mapping.DefaultAnalyzer = keyword.Name + index, err := NewMemOnly(mapping) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = index.Close() + }() + + query := NewQueryStringQuery("foo:bar +baz:\"\"") + searchReq := NewSearchRequest(query) + + _, _ = index.Search(searchReq) +} + +func TestDisjunctionQueryIncorrectMin(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + // create an index with default settings + idxMapping := NewIndexMapping() + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // create and insert documents as a batch + batch := idx.NewBatch() + docs := []struct { + field1 string + field2 int + }{ + { + field1: "one", + field2: 1, + }, + { + field1: "two", + field2: 2, + }, + } + + for i := 0; i < len(docs); i++ { + doc := document.NewDocument(strconv.Itoa(docs[i].field2)) + doc.Fields = []document.Field{ + document.NewTextField("field1", []uint64{}, []byte(docs[i].field1)), + document.NewNumericField("field2", []uint64{}, float64(docs[i].field2)), + } + doc.CompositeFields = []*document.CompositeField{ + document.NewCompositeFieldWithIndexingOptions( + "_all", true, []string{"text"}, []string{}, + index.IndexField|index.IncludeTermVectors), + } + if err = batch.IndexAdvanced(doc); err != nil { + t.Fatal(err) + } + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + tq := NewTermQuery("one") + dq := NewDisjunctionQuery(tq) + dq.SetMin(2) + sr := NewSearchRequestOptions(dq, 1, 0, false) + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if res.Total > 0 { + t.Fatalf("Expected 0 matches as disjunction query contains a single clause"+ + " but got: %v", res.Total) + } +} + +func TestMatchQueryPartialMatch(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + idx, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + doc1 := map[string]interface{}{ + "description": "Patrick is first name Stewart is last name", + } + doc2 := map[string]interface{}{ + "description": "Manager given name is Patrick", + } + batch := idx.NewBatch() + if err = batch.Index("doc1", doc1); err != nil { + t.Fatal(err) + } + if err = batch.Index("doc2", doc2); err != nil { + t.Fatal(err) + } + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + // Test 1 - Both Docs hit, doc 1 = Full Match and doc 2 = Partial Match + mq1 := NewMatchQuery("patrick stewart") + mq1.SetField("description") + + sr := NewSearchRequest(mq1) + sr.Explain = true + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if res.Total != 2 { + t.Errorf("Expected 2 results, but got: %v", res.Total) + } + for _, hit := range res.Hits { + switch hit.ID { + case "doc1": + if hit.Expl.PartialMatch { + t.Errorf("Expected doc1 to be a full match") + } + case "doc2": + if !hit.Expl.PartialMatch { + t.Errorf("Expected doc2 to be a partial match") + } + default: + t.Errorf("Unexpected document ID: %s", hit.ID) + } + } + + // Test 2 - Both Docs hit, doc 1 = Partial Match and doc 2 = Full Match + mq2 := NewMatchQuery("paltric manner") + mq2.SetField("description") + mq2.SetFuzziness(2) + + sr = NewSearchRequest(mq2) + sr.Explain = true + res, err = idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if res.Total != 2 { + t.Errorf("Expected 2 results, but got: %v", res.Total) + } + for _, hit := range res.Hits { + switch hit.ID { + case "doc1": + if !hit.Expl.PartialMatch { + t.Errorf("Expected doc1 to be a partial match") + } + case "doc2": + if hit.Expl.PartialMatch { + t.Errorf("Expected doc2 to be a full match") + } + default: + t.Errorf("Unexpected document ID: %s", hit.ID) + } + } + // Test 3 - Two Docs hits, both full match + mq3 := NewMatchQuery("patrick") + mq3.SetField("description") + + sr = NewSearchRequest(mq3) + sr.Explain = true + res, err = idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if res.Total != 2 { + t.Errorf("Expected 2 results, but got: %v", res.Total) + } + for _, hit := range res.Hits { + switch hit.ID { + case "doc1": + if hit.Expl.PartialMatch { + t.Errorf("Expected doc1 to be a full match") + } + case "doc2": + if hit.Expl.PartialMatch { + t.Errorf("Expected doc2 to be a full match") + } + default: + t.Errorf("Unexpected document ID: %s", hit.ID) + } + } + // Test 4 - Two Docs hits, both partial match + mq4 := NewMatchQuery("patrick stewart manager") + mq4.SetField("description") + + sr = NewSearchRequest(mq4) + sr.Explain = true + res, err = idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if res.Total != 2 { + t.Errorf("Expected 2 results, but got: %v", res.Total) + } + for _, hit := range res.Hits { + switch hit.ID { + case "doc1": + if !hit.Expl.PartialMatch { + t.Errorf("Expected doc1 to be a full match") + } + case "doc2": + if !hit.Expl.PartialMatch { + t.Errorf("Expected doc2 to be a full match") + } + default: + t.Errorf("Unexpected document ID: %s", hit.ID) + } + } + + // Test 5 - Match Query AND operator always results in full match + mq5 := NewMatchQuery("patrick stewart") + mq5.SetField("description") + mq5.SetOperator(1) + + sr = NewSearchRequest(mq5) + sr.Explain = true + res, err = idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if res.Total != 1 { + t.Errorf("Expected 1 result, but got: %v", res.Total) + } + hit := res.Hits[0] + if hit.ID != "doc1" || hit.Expl.PartialMatch { + t.Errorf("Expected doc1 to be a full match") + } +} + +func TestBooleanShouldMinPropagation(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc1 := map[string]interface{}{ + "dept": "queen", + "name": "cersei lannister", + } + + doc2 := map[string]interface{}{ + "dept": "kings guard", + "name": "jaime lannister", + } + + batch := idx.NewBatch() + + if err = batch.Index("doc1", doc1); err != nil { + t.Fatal(err) + } + + if err = batch.Index("doc2", doc2); err != nil { + t.Fatal(err) + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + // term dictionaries in the index for field.. + // dept: queen kings guard + // name: cersei jaime lannister + + // the following match query would match doc2 + mq1 := NewMatchQuery("kings guard") + mq1.SetField("dept") + + // the following match query would match both doc1 and doc2, + // as both docs share common lastname + mq2 := NewMatchQuery("jaime lannister") + mq2.SetField("name") + + bq := NewBooleanQuery() + bq.AddShould(mq1) + bq.AddMust(mq2) + + sr := NewSearchRequest(bq) + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if res.Total != 2 { + t.Errorf("Expected 2 results, but got: %v", res.Total) + } +} + +func TestDisjunctionMinPropagation(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, NewIndexMapping()) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc1 := map[string]interface{}{ + "dept": "finance", + "name": "xyz", + } + + doc2 := map[string]interface{}{ + "dept": "marketing", + "name": "xyz", + } + + doc3 := map[string]interface{}{ + "dept": "engineering", + "name": "abc", + } + + batch := idx.NewBatch() + + if err = batch.Index("doc1", doc1); err != nil { + t.Fatal(err) + } + + if err = batch.Index("doc2", doc2); err != nil { + t.Fatal(err) + } + + if err = batch.Index("doc3", doc3); err != nil { + t.Fatal(err) + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + mq1 := NewMatchQuery("finance") + mq2 := NewMatchQuery("marketing") + dq := NewDisjunctionQuery(mq1, mq2) + dq.SetMin(3) + + dq2 := NewDisjunctionQuery(dq) + dq2.SetMin(1) + + sr := NewSearchRequest(dq2) + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if res.Total != 0 { + t.Fatalf("Expect 0 results, but got: %v", res.Total) + } +} + +func TestDuplicateLocationsIssue1168(t *testing.T) { + fm1 := NewTextFieldMapping() + fm1.Analyzer = keyword.Name + fm1.Name = "name1" + + dm := NewDocumentStaticMapping() + dm.AddFieldMappingsAt("name", fm1) + + m := NewIndexMapping() + m.DefaultMapping = dm + + idx, err := NewMemOnly(m) + if err != nil { + t.Fatalf("bleve new err: %v", err) + } + + err = idx.Index("x", map[string]interface{}{ + "name": "marty", + }) + if err != nil { + t.Fatalf("bleve index err: %v", err) + } + + q1 := NewTermQuery("marty") + q2 := NewTermQuery("marty") + dq := NewDisjunctionQuery(q1, q2) + + sreq := NewSearchRequest(dq) + sreq.Fields = []string{"*"} + sreq.Highlight = NewHighlightWithStyle(html.Name) + + sres, err := idx.Search(sreq) + if err != nil { + t.Fatalf("bleve search err: %v", err) + } + if len(sres.Hits[0].Locations["name1"]["marty"]) != 1 { + t.Fatalf("duplicate marty") + } +} + +func TestBooleanMustSingleMatchNone(t *testing.T) { + idxMapping := NewIndexMapping() + if err := idxMapping.AddCustomTokenFilter(length.Name, map[string]interface{}{ + "min": 3.0, + "max": 5.0, + "type": length.Name, + }); err != nil { + t.Fatal(err) + } + if err := idxMapping.AddCustomAnalyzer("custom1", map[string]interface{}{ + "type": "custom", + "tokenizer": "single", + "token_filters": []interface{}{length.Name}, + }); err != nil { + t.Fatal(err) + } + + idxMapping.DefaultAnalyzer = "custom1" + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := map[string]interface{}{ + "languages_known": "Dutch", + "dept": "Sales", + } + + batch := idx.NewBatch() + if err = batch.Index("doc", doc); err != nil { + t.Fatal(err) + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + // this is a successful match + matchSales := NewMatchQuery("Sales") + matchSales.SetField("dept") + + // this would spin off a MatchNoneSearcher as the + // token filter rules out the word "French" + matchFrench := NewMatchQuery("French") + matchFrench.SetField("languages_known") + + bq := NewBooleanQuery() + bq.AddShould(matchSales) + bq.AddMust(matchFrench) + + sr := NewSearchRequest(bq) + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if res.Total != 0 { + t.Fatalf("Expected 0 results but got: %v", res.Total) + } +} + +func TestBooleanMustNotSingleMatchNone(t *testing.T) { + idxMapping := NewIndexMapping() + if err := idxMapping.AddCustomTokenFilter(shingle.Name, map[string]interface{}{ + "min": 3.0, + "max": 5.0, + "type": shingle.Name, + }); err != nil { + t.Fatal(err) + } + if err := idxMapping.AddCustomAnalyzer("custom1", map[string]interface{}{ + "type": "custom", + "tokenizer": "unicode", + "token_filters": []interface{}{shingle.Name}, + }); err != nil { + t.Fatal(err) + } + + idxMapping.DefaultAnalyzer = "custom1" + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := map[string]interface{}{ + "languages_known": "Dutch", + "dept": "Sales", + } + + batch := idx.NewBatch() + if err = batch.Index("doc", doc); err != nil { + t.Fatal(err) + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + // this is a successful match + matchSales := NewMatchQuery("Sales") + matchSales.SetField("dept") + + // this would spin off a MatchNoneSearcher as the + // token filter rules out the word "Dutch" + matchDutch := NewMatchQuery("Dutch") + matchDutch.SetField("languages_known") + + matchEngineering := NewMatchQuery("Engineering") + matchEngineering.SetField("dept") + + bq := NewBooleanQuery() + bq.AddShould(matchSales) + bq.AddMustNot(matchDutch, matchEngineering) + + sr := NewSearchRequest(bq) + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if res.Total != 0 { + t.Fatalf("Expected 0 results but got: %v", res.Total) + } +} + +func TestBooleanSearchBug1185(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + of := NewTextFieldMapping() + of.Analyzer = keyword.Name + of.Name = "owner" + + dm := NewDocumentMapping() + dm.AddFieldMappingsAt("owner", of) + + m := NewIndexMapping() + m.DefaultMapping = dm + + idx, err := NewUsing(tmpIndexPath, m, "scorch", "scorch", nil) + if err != nil { + t.Fatal(err) + } + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + err = idx.Index("17112", map[string]interface{}{ + "owner": "marty", + "type": "A Demo Type", + }) + if err != nil { + t.Fatal(err) + } + + err = idx.Index("17139", map[string]interface{}{ + "type": "A Demo Type", + }) + if err != nil { + t.Fatal(err) + } + + err = idx.Index("177777", map[string]interface{}{ + "type": "x", + }) + if err != nil { + t.Fatal(err) + } + err = idx.Index("177778", map[string]interface{}{ + "type": "A Demo Type", + }) + if err != nil { + t.Fatal(err) + } + + err = idx.Index("17140", map[string]interface{}{ + "type": "A Demo Type", + }) + if err != nil { + t.Fatal(err) + } + + err = idx.Index("17000", map[string]interface{}{ + "owner": "marty", + "type": "x", + }) + if err != nil { + t.Fatal(err) + } + + err = idx.Index("17141", map[string]interface{}{ + "type": "A Demo Type", + }) + if err != nil { + t.Fatal(err) + } + + err = idx.Index("17428", map[string]interface{}{ + "owner": "marty", + "type": "A Demo Type", + }) + if err != nil { + t.Fatal(err) + } + + err = idx.Index("17113", map[string]interface{}{ + "owner": "marty", + "type": "x", + }) + if err != nil { + t.Fatal(err) + } + + matchTypeQ := NewMatchPhraseQuery("A Demo Type") + matchTypeQ.SetField("type") + + matchAnyOwnerRegQ := NewRegexpQuery(".+") + matchAnyOwnerRegQ.SetField("owner") + + matchNoOwner := NewBooleanQuery() + matchNoOwner.AddMustNot(matchAnyOwnerRegQ) + + notNoOwner := NewBooleanQuery() + notNoOwner.AddMustNot(matchNoOwner) + + matchTypeAndNoOwner := NewConjunctionQuery() + matchTypeAndNoOwner.AddQuery(matchTypeQ) + matchTypeAndNoOwner.AddQuery(notNoOwner) + + req := NewSearchRequest(matchTypeAndNoOwner) + res, err := idx.Search(req) + if err != nil { + t.Fatal(err) + } + + // query 2 + matchTypeAndNoOwnerBoolean := NewBooleanQuery() + matchTypeAndNoOwnerBoolean.AddMust(matchTypeQ) + matchTypeAndNoOwnerBoolean.AddMustNot(matchNoOwner) + + req2 := NewSearchRequest(matchTypeAndNoOwnerBoolean) + res2, err := idx.Search(req2) + if err != nil { + t.Fatal(err) + } + + if len(res.Hits) != len(res2.Hits) { + t.Fatalf("expected same number of hits, got: %d and %d", len(res.Hits), len(res2.Hits)) + } +} + +func TestSearchScoreNone(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := NewUsing(tmpIndexPath, NewIndexMapping(), scorch.Name, Config.DefaultKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := map[string]interface{}{ + "field1": "asd fgh jkl", + "field2": "more content blah blah", + "id": "doc", + } + + if err = idx.Index("doc", doc); err != nil { + t.Fatal(err) + } + + q := NewQueryStringQuery("content") + sr := NewSearchRequest(q) + sr.IncludeLocations = true + sr.Score = "none" + + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if len(res.Hits) != 1 { + t.Fatal("unexpected number of hits") + } + + if len(res.Hits[0].Locations) != 1 { + t.Fatal("unexpected locations for the hit") + } + + if res.Hits[0].Score != 0 { + t.Fatal("unexpected score for the hit") + } +} + +func TestGeoDistanceIssue1301(t *testing.T) { + shopMapping := NewDocumentMapping() + shopMapping.AddFieldMappingsAt("GEO", NewGeoPointFieldMapping()) + shopIndexMapping := NewIndexMapping() + shopIndexMapping.DefaultMapping = shopMapping + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := NewUsing(tmpIndexPath, shopIndexMapping, scorch.Name, Config.DefaultKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + for i, g := range []string{"wecpkbeddsmf", "wecpk8tne453", "wecpkb80s09t"} { + if err = idx.Index(strconv.Itoa(i), map[string]interface{}{ + "ID": i, + "GEO": g, + }); err != nil { + t.Fatal(err) + } + } + + // Not setting "Field" for the following query, targets it against the _all + // field and this is returning inconsistent results, when there's another + // field indexed along with the geopoint which is numeric. + // As reported in: https://github.com/blevesearch/bleve/issues/1301 + lat, lon := 22.371154, 114.112603 + q := NewGeoDistanceQuery(lon, lat, "1km") + + req := NewSearchRequest(q) + sr, err := idx.Search(req) + if err != nil { + t.Fatal(err) + } + + if sr.Total != 3 { + t.Fatalf("Size expected: 3, actual %d\n", sr.Total) + } +} + +func TestSearchHighlightingWithRegexpReplacement(t *testing.T) { + idxMapping := NewIndexMapping() + if err := idxMapping.AddCustomCharFilter(regexp_char_filter.Name, map[string]interface{}{ + "regexp": `([a-z])\s+(\d)`, + "replace": "ooooo$1-$2", + "type": regexp_char_filter.Name, + }); err != nil { + t.Fatal(err) + } + if err := idxMapping.AddCustomAnalyzer("regexp_replace", map[string]interface{}{ + "type": custom.Name, + "tokenizer": "unicode", + "char_filters": []string{ + regexp_char_filter.Name, + }, + }); err != nil { + t.Fatal(err) + } + + idxMapping.DefaultAnalyzer = "regexp_replace" + idxMapping.StoreDynamic = true + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := NewUsing(tmpIndexPath, idxMapping, scorch.Name, Config.DefaultKVStore, nil) + if err != nil { + t.Fatal(err) + } + + defer func() { + err := idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := map[string]interface{}{ + "status": "fool 10", + } + + batch := idx.NewBatch() + if err = batch.Index("doc", doc); err != nil { + t.Fatal(err) + } + + if err = idx.Batch(batch); err != nil { + t.Fatal(err) + } + + query := NewMatchQuery("fool 10") + sreq := NewSearchRequest(query) + sreq.Fields = []string{"*"} + sreq.Highlight = NewHighlightWithStyle(ansi.Name) + + sres, err := idx.Search(sreq) + if err != nil { + t.Fatal(err) + } + + if sres.Total != 1 { + t.Fatalf("Expected 1 hit, got: %v", sres.Total) + } +} + +func TestAnalyzerInheritance(t *testing.T) { + tests := []struct { + name string + mappingStr string + doc map[string]interface{} + queryField string + queryTerm string + }{ + { + /* + index_mapping: keyword + default_mapping: "" + -> child field (should inherit keyword) + */ + name: "Child field to inherit index mapping's default analyzer", + mappingStr: `{"default_mapping":{"enabled":true,"dynamic":false,"properties":` + + `{"city":{"enabled":true,"dynamic":false,"fields":[{"name":"city","type":"text",` + + `"store":false,"index":true}]}}},"default_analyzer":"keyword"}`, + doc: map[string]interface{}{"city": "San Francisco"}, + queryField: "city", + queryTerm: "San Francisco", + }, + { + /* + index_mapping: standard + default_mapping: keyword + -> child field (should inherit keyword) + */ + name: "Child field to inherit default mapping's default analyzer", + mappingStr: `{"default_mapping":{"enabled":true,"dynamic":false,"properties":` + + `{"city":{"enabled":true,"dynamic":false,"fields":[{"name":"city","type":"text",` + + `"index":true}]}},"default_analyzer":"keyword"},"default_analyzer":"standard"}`, + doc: map[string]interface{}{"city": "San Francisco"}, + queryField: "city", + queryTerm: "San Francisco", + }, + { + /* + index_mapping: standard + default_mapping: keyword (dynamic) + -> search over field to (should inherit keyword) + */ + name: "Child field to inherit default mapping's default analyzer", + mappingStr: `{"default_mapping":{"enabled":true,"dynamic":true,"default_analyzer":"keyword"}` + + `,"default_analyzer":"standard"}`, + doc: map[string]interface{}{"city": "San Francisco"}, + queryField: "city", + queryTerm: "San Francisco", + }, + { + /* + index_mapping: standard + default_mapping: keyword + -> child mapping: "" + -> child field: (should inherit keyword) + */ + name: "Nested child field to inherit default mapping's default analyzer", + mappingStr: `{"default_mapping":{"enabled":true,"dynamic":false,"default_analyzer":` + + `"keyword","properties":{"address":{"enabled":true,"dynamic":false,"properties":` + + `{"city":{"enabled":true,"dynamic":false,"fields":[{"name":"city","type":"text",` + + `"index":true}]}}}}},"default_analyzer":"standard"}`, + doc: map[string]interface{}{ + "address": map[string]interface{}{"city": "San Francisco"}, + }, + queryField: "address.city", + queryTerm: "San Francisco", + }, + { + /* + index_mapping: standard + default_mapping: "" + -> child mapping: "keyword" + -> child mapping: "" + -> child field: (should inherit keyword) + */ + name: "Nested child field to inherit first child mapping's default analyzer", + mappingStr: `{"default_mapping":{"enabled":true,"dynamic":false,"properties":` + + `{"address":{"enabled":true,"dynamic":false,"default_analyzer":"keyword",` + + `"properties":{"state":{"enabled":true,"dynamic":false,"properties":{"city":` + + `{"enabled":true,"dynamic":false,"fields":[{"name":"city","type":"text",` + + `"store":false,"index":true}]}}}}}}},"default_analyer":"standard"}`, + doc: map[string]interface{}{ + "address": map[string]interface{}{ + "state": map[string]interface{}{"city": "San Francisco"}, + }, + }, + queryField: "address.state.city", + queryTerm: "San Francisco", + }, + } + + for i := range tests { + t.Run(tests[i].name, func(t *testing.T) { + idxMapping := NewIndexMapping() + if err := idxMapping.UnmarshalJSON([]byte(tests[i].mappingStr)); err != nil { + t.Fatal(err) + } + + tmpIndexPath := createTmpIndexPath(t) + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + if err := idx.Close(); err != nil { + t.Fatal(err) + } + }() + + if err = idx.Index("doc", tests[i].doc); err != nil { + t.Fatal(err) + } + + q := NewTermQuery(tests[i].queryTerm) + q.SetField(tests[i].queryField) + + res, err := idx.Search(NewSearchRequest(q)) + if err != nil { + t.Fatal(err) + } + + if len(res.Hits) != 1 { + t.Errorf("Unexpected number of hits: %v", len(res.Hits)) + } + }) + } +} + +func TestHightlightingWithHTMLCharacterFilter(t *testing.T) { + idxMapping := NewIndexMapping() + if err := idxMapping.AddCustomAnalyzer("custom-html", map[string]interface{}{ + "type": custom.Name, + "tokenizer": "unicode", + "char_filters": []interface{}{html_char_filter.Name}, + }); err != nil { + t.Fatal(err) + } + + fm := mapping.NewTextFieldMapping() + fm.Analyzer = "custom-html" + + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("content", fm) + + idxMapping.DefaultMapping = dmap + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + content := "
Welcome to blevesearch.
" + if err = idx.Index("doc", map[string]string{ + "content": content, + }); err != nil { + t.Fatal(err) + } + + searchStr := "blevesearch" + q := query.NewMatchQuery(searchStr) + q.SetField("content") + sr := NewSearchRequest(q) + sr.IncludeLocations = true + sr.Fields = []string{"*"} + sr.Highlight = NewHighlightWithStyle(html.Name) + searchResults, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if len(searchResults.Hits) != 1 || + len(searchResults.Hits[0].Locations["content"][searchStr]) != 1 { + t.Fatalf("Expected 1 hit with 1 location") + } + + expectedLocation := &search.Location{ + Pos: 3, + Start: uint64(strings.Index(content, searchStr)), + End: uint64(strings.Index(content, searchStr) + len(searchStr)), + } + expectedFragment := "<div> Welcome to blevesearch. </div>" + + gotLocation := searchResults.Hits[0].Locations["content"]["blevesearch"][0] + gotFragment := searchResults.Hits[0].Fragments["content"][0] + + if !reflect.DeepEqual(expectedLocation, gotLocation) { + t.Fatalf("Mismatch in locations, got: %v, expected: %v", + gotLocation, expectedLocation) + } + + if expectedFragment != gotFragment { + t.Fatalf("Mismatch in fragment, got: %v, expected: %v", + gotFragment, expectedFragment) + } +} + +func TestIPRangeQuery(t *testing.T) { + idxMapping := NewIndexMapping() + im := NewIPFieldMapping() + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("ip_content", im) + idxMapping.DefaultMapping = dmap + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + ipContent := "192.168.10.11" + if err = idx.Index("doc", map[string]string{ + "ip_content": ipContent, + }); err != nil { + t.Fatal(err) + } + + q := query.NewIPRangeQuery("192.168.10.0/24") + q.SetField("ip_content") + sr := NewSearchRequest(q) + + searchResults, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if len(searchResults.Hits) != 1 || + searchResults.Hits[0].ID != "doc" { + t.Fatal("Expected the 1 result - doc") + } +} + +func TestGeoShapePolygonContainsPoint(t *testing.T) { + fm := mapping.NewGeoShapeFieldMapping() + dmap := mapping.NewDocumentMapping() + dmap.AddFieldMappingsAt("geometry", fm) + + idxMapping := NewIndexMapping() + idxMapping.DefaultMapping = dmap + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + // Polygon coordinates to be ordered in counter-clock-wise order + // for the outer loop, and holes to follow clock-wise order. + // See: https://www.rfc-editor.org/rfc/rfc7946.html#section-3.1.6 + + one := []byte(`{ + "geometry":{ + "type":"Polygon", + "coordinates":[[ + [4.8089,46.9307], + [4.8223,46.8915], + [4.8149,46.886], + [4.8252,46.8647], + [4.8305,46.8531], + [4.8506,46.8509], + [4.8574,46.8621], + [4.8576,46.8769], + [4.8753,46.8774], + [4.8909,46.8519], + [4.8837,46.8485], + [4.9014,46.8318], + [4.9067,46.8179], + [4.8986,46.8122], + [4.9081,46.7969], + [4.9535,46.8254], + [4.9577,46.8053], + [5.0201,46.821], + [5.0357,46.8207], + [5.0656,46.8434], + [5.0955,46.8411], + [5.1149,46.8435], + [5.1259,46.8395], + [5.1433,46.8463], + [5.1415,46.8589], + [5.1533,46.873], + [5.138,46.8843], + [5.1525,46.9012], + [5.1485,46.9165], + [5.1582,46.926], + [5.1882,46.9251], + [5.2039,46.9129], + [5.2223,46.9175], + [5.2168,46.926], + [5.2338,46.9316], + [5.228,46.9505], + [5.2078,46.9722], + [5.2117,46.98], + [5.1961,46.9783], + [5.1663,46.9638], + [5.1213,46.9634], + [5.1086,46.9596], + [5.0729,46.9604], + [5.0731,46.9668], + [5.0493,46.9817], + [5.0034,46.9722], + [4.9852,46.9585], + [4.9479,46.9664], + [4.8943,46.9663], + [4.8937,46.951], + [4.8534,46.9458], + [4.8089,46.9307] + ]] + } + }`) + + two := []byte(`{ + "geometry":{ + "type":"Polygon", + "coordinates":[[ + [2.2266,48.7816], + [2.2266,48.7761], + [2.2288,48.7745], + [2.2717,48.7905], + [2.2799,48.8109], + [2.3013,48.8251], + [2.2894,48.8283], + [2.2726,48.8144], + [2.2518,48.8164], + [2.255,48.8101], + [2.2348,48.7954], + [2.2266,48.7816] + ]] + } + }`) + + var doc1, doc2 map[string]interface{} + + if err = json.Unmarshal(one, &doc1); err != nil { + t.Fatal(err) + } + if err = idx.Index("doc1", doc1); err != nil { + t.Fatal(err) + } + + if err = json.Unmarshal(two, &doc2); err != nil { + t.Fatal(err) + } + if err = idx.Index("doc2", doc2); err != nil { + t.Fatal(err) + } + + for testi, test := range []struct { + coordinates []float64 + expectHits []string + }{ + { + coordinates: []float64{5, 46.9}, + expectHits: []string{"doc1"}, + }, + { + coordinates: []float64{1.5, 48.2}, + }, + } { + q, err := NewGeoShapeQuery( + [][][][]float64{{{test.coordinates}}}, + geo.PointType, + "contains", + ) + if err != nil { + t.Fatalf("test: %d, query err: %v", testi+1, err) + } + q.SetField("geometry") + + res, err := idx.Search(NewSearchRequest(q)) + if err != nil { + t.Fatalf("test: %d, search err: %v", testi+1, err) + } + + if len(res.Hits) != len(test.expectHits) { + t.Errorf("test: %d, unexpected hits: %v", testi+1, len(res.Hits)) + } + + OUTER: + for _, expect := range test.expectHits { + for _, got := range res.Hits { + if got.ID == expect { + continue OUTER + } + } + t.Errorf("test: %d, couldn't get: %v", testi+1, expect) + } + } +} + +func TestAnalyzerInheritanceForDefaultDynamicMapping(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + imap := mapping.NewIndexMapping() + imap.DefaultMapping.DefaultAnalyzer = keyword.Name + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + doc := map[string]interface{}{ + "fieldX": "AbCdEf", + } + + if err = idx.Index("doc", doc); err != nil { + t.Fatal(err) + } + + // Match query to apply keyword analyzer to fieldX. + mq := NewMatchQuery("AbCdEf") + mq.SetField("fieldX") + + sr := NewSearchRequest(mq) + results, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + + if len(results.Hits) != 1 { + t.Fatalf("expected 1 hit, got %d", len(results.Hits)) + } +} + +func TestCustomDateTimeParserLayoutValidation(t *testing.T) { + flexiblegoName := flexible.Name + sanitizedgoName := sanitized.Name + imap := mapping.NewIndexMapping() + correctConfig := map[string]interface{}{ + "type": sanitizedgoName, + "layouts": []interface{}{ + // some custom layouts + "2006-01-02 15:04:05.0000", + "2006\\01\\02T03:04:05PM", + "2006/01/02", + "2006-01-02T15:04:05.999Z0700PMMST", + "15:04:05.0000Z07:00 Monday", + + // standard layouts + time.Layout, + time.ANSIC, + time.UnixDate, + time.RubyDate, + time.RFC822, + time.RFC822Z, + time.RFC850, + time.RFC1123, + time.RFC1123Z, + time.RFC3339, + time.RFC3339Nano, + time.Kitchen, + time.Stamp, + time.StampMilli, + time.StampMicro, + time.StampNano, + "2006-01-02 15:04:05", // time.DateTime + "2006-01-02", // time.DateOnly + "15:04:05", // time.TimeOnly + + // Corrected layouts to the incorrect ones below. + "2006-01-02 03:04:05 -0700", + "2006-01-02 15:04:05 -0700", + "3:04PM", + "2006-01-02 15:04:05.000 -0700 MST", + "January 2 2006 3:04 PM", + "02/Jan/06 3:04PM", + "Mon 02 Jan 3:04:05 PM", + }, + } + + // Correct layouts - sanitizedgo should work without errors. + err := imap.AddCustomDateTimeParser("custDT", correctConfig) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + // Flexiblego should work without errors as well. + correctConfig["type"] = flexiblegoName + err = imap.AddCustomDateTimeParser("custDT_Flexi", correctConfig) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + incorrectLayouts := [][]interface{}{ + { + "2000-03-31 01:33:51 +0300", + }, + { + "2006-01-02 15:04:51 +0300", + }, + { + "2000-03-31 01:33:05 +0300", + }, + { + "4:45PM", + }, + { + "2006-01-02 15:04:05.445 -0700 MST", + }, + { + "August 20 2001 8:55 AM", + }, + { + "28/Jul/23 12:48PM", + }, + { + "Tue 22 Aug 6:37:30 AM", + }, + } + + // first check sanitizedgo, should throw error for each of the incorrect layouts. + numExpectedErrors := len(incorrectLayouts) + numActualErrors := 0 + for idx, badLayout := range incorrectLayouts { + incorrectConfig := map[string]interface{}{ + "type": sanitizedgoName, + "layouts": badLayout, + } + err := imap.AddCustomDateTimeParser(fmt.Sprintf("%d_DT", idx), incorrectConfig) + if err != nil { + numActualErrors++ + } + } + // Expecting all layouts to be incorrect, since sanitizedgo is being used. + if numActualErrors != numExpectedErrors { + t.Fatalf("expected %d errors, got: %d", numExpectedErrors, numActualErrors) + } + + // sanity test - flexiblego should still allow the incorrect layouts, for legacy purposes + for idx, badLayout := range incorrectLayouts { + incorrectConfig := map[string]interface{}{ + "type": flexiblegoName, + "layouts": badLayout, + } + err := imap.AddCustomDateTimeParser(fmt.Sprintf("%d_DT_Flexi", idx), incorrectConfig) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + } +} + +func TestDateRangeStringQuery(t *testing.T) { + idxMapping := NewIndexMapping() + + err := idxMapping.AddCustomDateTimeParser("customDT", map[string]interface{}{ + "type": sanitized.Name, + "layouts": []interface{}{ + "02/01/2006 15:04:05", + "2006/01/02 3:04PM", + }, + }) + if err != nil { + t.Fatal(err) + } + + err = idxMapping.AddCustomDateTimeParser("queryDT", map[string]interface{}{ + "type": sanitized.Name, + "layouts": []interface{}{ + "02/01/2006 3:04PM", + }, + }) + if err != nil { + t.Fatal(err) + } + + dtmap := NewDateTimeFieldMapping() + dtmap.DateFormat = "customDT" + idxMapping.DefaultMapping.AddFieldMappingsAt("date", dtmap) + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + documents := map[string]map[string]interface{}{ + "doc1": { + "date": "2001/08/20 6:00PM", + }, + "doc2": { + "date": "20/08/2001 18:00:20", + }, + "doc3": { + "date": "20/08/2001 18:10:00", + }, + "doc4": { + "date": "2001/08/20 6:15PM", + }, + "doc5": { + "date": "20/08/2001 18:20:00", + }, + } + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + type testResult struct { + docID string // doc ID of the hit + hitField string // fields returned as part of the hit + } + + type testStruct struct { + start string + end string + field string + dateTimeParser string // name of the custom date time parser to use if nil, use QueryDateTimeParser + includeStart bool + includeEnd bool + expectedHits []testResult + err error + } + + testQueries := []testStruct{ + // test cases with RFC3339 parser and toggling includeStart and includeEnd + { + start: "2001-08-20T18:00:00", + end: "2001-08-20T18:10:00", + field: "date", + includeStart: true, + includeEnd: true, + expectedHits: []testResult{ + { + docID: "doc1", + hitField: "2001/08/20 6:00PM", + }, + { + docID: "doc2", + hitField: "20/08/2001 18:00:20", + }, + { + docID: "doc3", + hitField: "20/08/2001 18:10:00", + }, + }, + }, + { + start: "2001-08-20T18:00:00", + end: "2001-08-20T18:10:00", + field: "date", + includeStart: false, + includeEnd: true, + expectedHits: []testResult{ + { + docID: "doc2", + hitField: "20/08/2001 18:00:20", + }, + { + docID: "doc3", + hitField: "20/08/2001 18:10:00", + }, + }, + }, + { + start: "2001-08-20T18:00:00", + end: "2001-08-20T18:10:00", + field: "date", + includeStart: false, + includeEnd: false, + expectedHits: []testResult{ + { + docID: "doc2", + hitField: "20/08/2001 18:00:20", + }, + }, + }, + // test cases with custom parser and omitting start and end + { + start: "20/08/2001 18:00:00", + end: "2001/08/20 6:10PM", + field: "date", + dateTimeParser: "customDT", + includeStart: true, + includeEnd: true, + expectedHits: []testResult{ + { + docID: "doc1", + hitField: "2001/08/20 6:00PM", + }, + { + docID: "doc2", + hitField: "20/08/2001 18:00:20", + }, + { + docID: "doc3", + hitField: "20/08/2001 18:10:00", + }, + }, + }, + { + end: "20/08/2001 18:15:00", + field: "date", + dateTimeParser: "customDT", + includeStart: true, + includeEnd: true, + expectedHits: []testResult{ + { + docID: "doc1", + hitField: "2001/08/20 6:00PM", + }, + { + docID: "doc2", + hitField: "20/08/2001 18:00:20", + }, + { + docID: "doc3", + hitField: "20/08/2001 18:10:00", + }, + { + docID: "doc4", + hitField: "2001/08/20 6:15PM", + }, + }, + }, + { + start: "2001/08/20 6:15PM", + field: "date", + dateTimeParser: "customDT", + includeStart: true, + includeEnd: true, + expectedHits: []testResult{ + { + docID: "doc4", + hitField: "2001/08/20 6:15PM", + }, + { + docID: "doc5", + hitField: "20/08/2001 18:20:00", + }, + }, + }, + { + start: "20/08/2001 6:15PM", + field: "date", + dateTimeParser: "queryDT", + includeStart: true, + includeEnd: true, + expectedHits: []testResult{ + { + docID: "doc4", + hitField: "2001/08/20 6:15PM", + }, + { + docID: "doc5", + hitField: "20/08/2001 18:20:00", + }, + }, + }, + // error path test cases + { + field: "date", + dateTimeParser: "customDT", + includeStart: true, + includeEnd: true, + err: fmt.Errorf("date range query must specify at least one of start/end"), + }, + { + field: "date", + includeStart: true, + includeEnd: true, + err: fmt.Errorf("date range query must specify at least one of start/end"), + }, + { + start: "2001-08-20T18:00:00", + end: "2001-08-20T18:10:00", + field: "date", + dateTimeParser: "customDT", + err: fmt.Errorf("unable to parse datetime with any of the layouts, date time parser name: customDT"), + }, + { + start: "3001-08-20T18:00:00", + end: "2001-08-20T18:10:00", + field: "date", + err: fmt.Errorf("invalid/unsupported date range, start: 3001-08-20T18:00:00"), + }, + { + start: "2001/08/20 6:00PM", + end: "3001/08/20 6:30PM", + field: "date", + dateTimeParser: "customDT", + err: fmt.Errorf("invalid/unsupported date range, end: 3001/08/20 6:30PM"), + }, + } + + for _, dtq := range testQueries { + var err error + dateQuery := NewDateRangeInclusiveStringQuery(dtq.start, dtq.end, &dtq.includeStart, &dtq.includeEnd) + dateQuery.SetDateTimeParser(dtq.dateTimeParser) + dateQuery.SetField(dtq.field) + + sr := NewSearchRequest(dateQuery) + sr.SortBy([]string{dtq.field}) + sr.Fields = []string{dtq.field} + + res, err := idx.Search(sr) + if err != nil { + if dtq.err == nil { + t.Fatalf("expected no error, got: %v", err) + } + if dtq.err.Error() != err.Error() { + t.Fatalf("expected error: %v, got: %v", dtq.err, err) + } + continue + } + if len(res.Hits) != len(dtq.expectedHits) { + t.Fatalf("expected %d hits, got %d", len(dtq.expectedHits), len(res.Hits)) + } + for i, hit := range res.Hits { + if hit.ID != dtq.expectedHits[i].docID { + t.Fatalf("expected docID %s, got %s", dtq.expectedHits[i].docID, hit.ID) + } + if hit.Fields[dtq.field].(string) != dtq.expectedHits[i].hitField { + t.Fatalf("expected hit field %s, got %s", dtq.expectedHits[i].hitField, hit.Fields[dtq.field]) + } + } + } +} + +func TestDateRangeFacetQueriesWithCustomDateTimeParser(t *testing.T) { + idxMapping := NewIndexMapping() + + err := idxMapping.AddCustomDateTimeParser("customDT", map[string]interface{}{ + "type": sanitized.Name, + "layouts": []interface{}{ + "02/01/2006 15:04:05", + "2006/01/02 3:04PM", + }, + }) + if err != nil { + t.Fatal(err) + } + + err = idxMapping.AddCustomDateTimeParser("queryDT", map[string]interface{}{ + "type": sanitized.Name, + "layouts": []interface{}{ + "02/01/2006 3:04PM", + }, + }) + if err != nil { + t.Fatal(err) + } + + dtmap := NewDateTimeFieldMapping() + dtmap.DateFormat = "customDT" + idxMapping.DefaultMapping.AddFieldMappingsAt("date", dtmap) + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, idxMapping) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + documents := map[string]map[string]interface{}{ + "doc1": { + "date": "2001/08/20 6:00PM", + }, + "doc2": { + "date": "20/08/2001 18:00:20", + }, + "doc3": { + "date": "20/08/2001 18:10:00", + }, + "doc4": { + "date": "2001/08/20 6:15PM", + }, + "doc5": { + "date": "20/08/2001 18:20:00", + }, + } + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + query := NewMatchAllQuery() + + type testFacetResult struct { + name string + start string + end string + count int + err error + } + + type testFacetRequest struct { + name string + start string + end string + parser string + result testFacetResult + } + + tests := []testFacetRequest{ + { + // Test without a query time override of the parser (use default parser) + name: "test", + start: "2001-08-20 18:00:00", + end: "2001-08-20 18:10:00", + result: testFacetResult{ + name: "test", + start: "2001-08-20T18:00:00Z", + end: "2001-08-20T18:10:00Z", + count: 2, + err: nil, + }, + }, + { + name: "test", + start: "20/08/2001 6:00PM", + end: "20/08/2001 6:10PM", + parser: "queryDT", + result: testFacetResult{ + name: "test", + start: "2001-08-20T18:00:00Z", + end: "2001-08-20T18:10:00Z", + count: 2, + err: nil, + }, + }, + { + name: "test", + start: "20/08/2001 15:00:00", + end: "2001/08/20 6:10PM", + parser: "customDT", + result: testFacetResult{ + name: "test", + start: "2001-08-20T15:00:00Z", + end: "2001-08-20T18:10:00Z", + count: 2, + err: nil, + }, + }, + { + name: "test", + end: "2001/08/20 6:15PM", + parser: "customDT", + result: testFacetResult{ + name: "test", + end: "2001-08-20T18:15:00Z", + count: 3, + err: nil, + }, + }, + { + name: "test", + start: "20/08/2001 6:15PM", + parser: "queryDT", + result: testFacetResult{ + name: "test", + start: "2001-08-20T18:15:00Z", + count: 2, + err: nil, + }, + }, + // some error cases + { + name: "test", + parser: "queryDT", + result: testFacetResult{ + name: "test", + err: fmt.Errorf("date range query must specify either start, end or both for date range name 'test'"), + }, + }, + { + // default parser is used for the query, but the start time is not in the correct format (RFC3339), + // so it should throw an error + name: "test", + start: "20/08/2001 6:15PM", + result: testFacetResult{ + name: "test", + err: fmt.Errorf("ParseDates err: error parsing start date '20/08/2001 6:15PM' for date range name 'test': unable to parse datetime with any of the layouts, using date time parser named dateTimeOptional"), + }, + }, + } + + for _, test := range tests { + searchRequest := NewSearchRequest(query) + + fr := NewFacetRequest("date", 100) + start := &test.start + if test.start == "" { + start = nil + } + end := &test.end + if test.end == "" { + end = nil + } + + fr.AddDateTimeRangeStringWithParser(test.name, start, end, test.parser) + searchRequest.AddFacet("dateFacet", fr) + + searchResults, err := idx.Search(searchRequest) + if err != nil { + if test.result.err == nil { + t.Fatalf("Unexpected error: %v", err) + } + if err.Error() != test.result.err.Error() { + t.Fatalf("Expected error %v, got %v", test.result.err, err) + } + continue + } + for _, facetResult := range searchResults.Facets { + if len(facetResult.DateRanges) != 1 { + t.Fatal("Expected 1 date range facet") + } + result := facetResult.DateRanges[0] + if result.Name != test.result.name { + t.Fatalf("Expected name %s, got %s", test.result.name, result.Name) + } + if result.Start != nil && *result.Start != test.result.start { + t.Fatalf("Expected start %s, got %s", test.result.start, *result.Start) + } + if result.End != nil && *result.End != test.result.end { + t.Fatalf("Expected end %s, got %s", test.result.end, *result.End) + } + if result.Start == nil && test.result.start != "" { + t.Fatalf("Expected start %s, got nil", test.result.start) + } + if result.End == nil && test.result.end != "" { + t.Fatalf("Expected end %s, got nil", test.result.end) + } + if result.Count != test.result.count { + t.Fatalf("Expected count %d, got %d", test.result.count, result.Count) + } + } + } +} + +func TestDateRangeTimestampQueries(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + imap := mapping.NewIndexMapping() + + // add a date field with a valid format to the default mapping + // for good measure + + dtParserConfig := map[string]interface{}{ + "type": flexible.Name, + "layouts": []interface{}{"2006/01/02 15:04:05"}, + } + err := imap.AddCustomDateTimeParser("custDT", dtParserConfig) + if err != nil { + t.Fatal(err) + } + + dateField := mapping.NewDateTimeFieldMapping() + dateField.DateFormat = "custDT" + + unixSecField := mapping.NewDateTimeFieldMapping() + unixSecField.DateFormat = seconds.Name + + unixMilliSecField := mapping.NewDateTimeFieldMapping() + unixMilliSecField.DateFormat = milliseconds.Name + + unixMicroSecField := mapping.NewDateTimeFieldMapping() + unixMicroSecField.DateFormat = microseconds.Name + + unixNanoSecField := mapping.NewDateTimeFieldMapping() + unixNanoSecField.DateFormat = nanoseconds.Name + + imap.DefaultMapping.AddFieldMappingsAt("date", dateField) + imap.DefaultMapping.AddFieldMappingsAt("seconds", unixSecField) + imap.DefaultMapping.AddFieldMappingsAt("milliseconds", unixMilliSecField) + imap.DefaultMapping.AddFieldMappingsAt("microseconds", unixMicroSecField) + imap.DefaultMapping.AddFieldMappingsAt("nanoseconds", unixNanoSecField) + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + documents := map[string]map[string]string{ + "doc1": { + "date": "2001/08/20 03:00:10", + "seconds": "998276410", + "milliseconds": "998276410100", + "microseconds": "998276410100300", + "nanoseconds": "998276410100300400", + }, + "doc2": { + "date": "2001/08/20 03:00:20", + "seconds": "998276420", + "milliseconds": "998276410200", + "microseconds": "998276410100400", + "nanoseconds": "998276410100300500", + }, + "doc3": { + "date": "2001/08/20 03:00:30", + "seconds": "998276430", + "milliseconds": "998276410300", + "microseconds": "998276410100500", + "nanoseconds": "998276410100300600", + }, + "doc4": { + "date": "2001/08/20 03:00:40", + "seconds": "998276440", + "milliseconds": "998276410400", + "microseconds": "998276410100600", + "nanoseconds": "998276410100300700", + }, + "doc5": { + "date": "2001/08/20 03:00:50", + "seconds": "998276450", + "milliseconds": "998276410500", + "microseconds": "998276410100700", + "nanoseconds": "998276410100300800", + }, + } + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + type testStruct struct { + start string + end string + field string + expectedHits []string + } + + testQueries := []testStruct{ + { + start: "2001-08-20T03:00:05", + end: "2001-08-20T03:00:25", + field: "date", + expectedHits: []string{ + "doc1", + "doc2", + }, + }, + { + start: "2001-08-20T03:00:15", + end: "2001-08-20T03:00:35", + field: "seconds", + expectedHits: []string{ + "doc2", + "doc3", + }, + }, + { + start: "2001-08-20T03:00:10.150", + end: "2001-08-20T03:00:10.450", + field: "milliseconds", + expectedHits: []string{ + "doc2", + "doc3", + "doc4", + }, + }, + { + start: "2001-08-20T03:00:10.100450", + end: "2001-08-20T03:00:10.100650", + field: "microseconds", + expectedHits: []string{ + "doc3", + "doc4", + }, + }, + { + start: "2001-08-20T03:00:10.100300550", + end: "2001-08-20T03:00:10.100300850", + field: "nanoseconds", + expectedHits: []string{ + "doc3", + "doc4", + "doc5", + }, + }, + } + testLayout := "2006-01-02T15:04:05" + for _, dtq := range testQueries { + startTime, err := time.Parse(testLayout, dtq.start) + if err != nil { + t.Fatal(err) + } + endTime, err := time.Parse(testLayout, dtq.end) + if err != nil { + t.Fatal(err) + } + drq := NewDateRangeQuery(startTime, endTime) + drq.SetField(dtq.field) + + sr := NewSearchRequest(drq) + sr.SortBy([]string{dtq.field}) + sr.Fields = []string{"*"} + + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if len(res.Hits) != len(dtq.expectedHits) { + t.Fatalf("expected %d hits, got %d", len(dtq.expectedHits), len(res.Hits)) + } + for i, hit := range res.Hits { + if hit.ID != dtq.expectedHits[i] { + t.Fatalf("expected docID %s, got %s", dtq.expectedHits[i], hit.ID) + } + if len(hit.Fields) != len(documents[hit.ID]) { + t.Fatalf("expected hit %s to have %d fields, got %d", hit.ID, len(documents[hit.ID]), len(hit.Fields)) + } + for k, v := range documents[hit.ID] { + if hit.Fields[k] != v { + t.Fatalf("expected field %s to be %s, got %s", k, v, hit.Fields[k]) + } + } + } + } +} + +func TestPercentAndIsoStyleDates(t *testing.T) { + percentName := percent.Name + isoName := iso.Name + + imap := mapping.NewIndexMapping() + percentConfig := map[string]interface{}{ + "type": percentName, + "layouts": []interface{}{ + "%Y/%m/%d %l:%M%p", // doc 1 + "%d/%m/%Y %H:%M:%S", // doc 2 + "%Y-%m-%dT%H:%M:%S%z", // doc 3 + "%d %B %y %l%p %Z", // doc 4 + "%Y; %b %d (%a) %I:%M:%S.%N%P %z", // doc 5 + }, + } + isoConfig := map[string]interface{}{ + "type": isoName, + "layouts": []interface{}{ + "yyyy/MM/dd h:mma", // doc 1 + "dd/MM/yyyy HH:mm:ss", // doc 2 + "yyyy-MM-dd'T'HH:mm:ssXX", // doc 3 + "dd MMMM yy ha z", // doc 4 + "yyyy; MMM dd (EEE) hh:mm:ss.SSSSSaa xx", // doc 5 + }, + } + + err := imap.AddCustomDateTimeParser("percentDate", percentConfig) + if err != nil { + t.Fatal(err) + } + err = imap.AddCustomDateTimeParser("isoDate", isoConfig) + if err != nil { + t.Fatal(err) + } + + percentField := mapping.NewDateTimeFieldMapping() + percentField.DateFormat = "percentDate" + + isoField := mapping.NewDateTimeFieldMapping() + isoField.DateFormat = "isoDate" + + imap.DefaultMapping.AddFieldMappingsAt("percentDate", percentField) + imap.DefaultMapping.AddFieldMappingsAt("isoDate", isoField) + + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + documents := map[string]map[string]interface{}{ + "doc1": { + "percentDate": "2001/08/20 6:00PM", + "isoDate": "2001/08/20 6:00PM", + }, + "doc2": { + "percentDate": "20/08/2001 18:05:00", + "isoDate": "20/08/2001 18:05:00", + }, + "doc3": { + "percentDate": "2001-08-20T18:10:00Z", + "isoDate": "2001-08-20T18:10:00Z", + }, + "doc4": { + "percentDate": "20 August 01 6PM UTC", + "isoDate": "20 August 01 6PM UTC", + }, + "doc5": { + "percentDate": "2001; Aug 20 (Mon) 06:15:15.23456pm +0000", + "isoDate": "2001; Aug 20 (Mon) 06:15:15.23456pm +0000", + }, + } + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + type testStruct struct { + start string + end string + field string + } + + for _, field := range []string{"percentDate", "isoDate"} { + testQueries := []testStruct{ + { + start: "2001/08/20 6:00PM", + end: "2001/08/20 6:20PM", + field: field, + }, + { + start: "20/08/2001 18:00:00", + end: "20/08/2001 18:20:00", + field: field, + }, + { + start: "2001-08-20T18:00:00Z", + end: "2001-08-20T18:20:00Z", + field: field, + }, + { + start: "20 August 01 6PM UTC", + end: "20 August 01 7PM UTC", + field: field, + }, + { + start: "2001; Aug 20 (Mon) 06:00:00.00000pm +0000", + end: "2001; Aug 20 (Mon) 06:20:20.00000pm +0000", + field: field, + }, + } + includeStart := true + includeEnd := true + for _, dtq := range testQueries { + drq := NewDateRangeInclusiveStringQuery(dtq.start, dtq.end, &includeStart, &includeEnd) + drq.SetField(dtq.field) + drq.SetDateTimeParser(field) + sr := NewSearchRequest(drq) + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if len(res.Hits) != 5 { + t.Fatalf("expected %d hits, got %d", 5, len(res.Hits)) + } + } + } +} + +func roundToDecimalPlace(num float64, decimalPlaces int) float64 { + precision := math.Pow(10, float64(decimalPlaces)) + return math.Round(num*precision) / precision +} + +func TestScoreBreakdown(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + imap := mapping.NewIndexMapping() + textField := mapping.NewTextFieldMapping() + textField.Analyzer = simple.Name + imap.DefaultMapping.AddFieldMappingsAt("text", textField) + + documents := map[string]map[string]interface{}{ + "doc1": { + "text": "lorem ipsum dolor sit amet consectetur adipiscing elit do eiusmod tempor", + }, + "doc2": { + "text": "lorem dolor amet adipiscing sed eiusmod", + }, + "doc3": { + "text": "ipsum sit consectetur elit do tempor", + }, + "doc4": { + "text": "lorem ipsum sit amet adipiscing elit do eiusmod", + }, + } + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + type testResult struct { + docID string // doc ID of the hit + score float64 + scoreBreakdown map[int]float64 + } + type testStruct struct { + query string + typ string + expectHits []testResult + } + testQueries := []testStruct{ + { + // trigger disjunction heap searcher (>10 searchers) + // expect score breakdown to have a 0 at BLANK + query: `{"disjuncts":[{"term":"lorem","field":"text"},{"term":"blank","field":"text"},{"term":"ipsum","field":"text"},{"term":"blank","field":"text"},{"term":"blank","field":"text"},{"term":"dolor","field":"text"},{"term":"sit","field":"text"},{"term":"amet","field":"text"},{"term":"consectetur","field":"text"},{"term":"blank","field":"text"},{"term":"adipiscing","field":"text"},{"term":"blank","field":"text"},{"term":"elit","field":"text"},{"term":"sed","field":"text"},{"term":"do","field":"text"},{"term":"eiusmod","field":"text"},{"term":"tempor","field":"text"},{"term":"blank","field":"text"},{"term":"blank","field":"text"}]}`, + typ: "disjunction", + expectHits: []testResult{ + { + docID: "doc1", + score: 0.3034548543819603, + scoreBreakdown: map[int]float64{0: 0.040398807605268316, 2: 0.040398807605268316, 5: 0.0669862776967768, 6: 0.040398807605268316, 7: 0.040398807605268316, 8: 0.0669862776967768, 10: 0.040398807605268316, 12: 0.040398807605268316, 14: 0.040398807605268316, 15: 0.040398807605268316, 16: 0.0669862776967768}, + }, + { + docID: "doc2", + score: 0.14725661652397853, + scoreBreakdown: map[int]float64{0: 0.05470024557900147, 5: 0.09069985124905133, 7: 0.05470024557900147, 10: 0.05470024557900147, 13: 0.15681178542754148, 15: 0.05470024557900147}, + }, + { + docID: "doc3", + score: 0.12637916362550797, + scoreBreakdown: map[int]float64{2: 0.05470024557900147, 6: 0.05470024557900147, 8: 0.09069985124905133, 12: 0.05470024557900147, 14: 0.05470024557900147, 16: 0.09069985124905133}, + }, + { + docID: "doc4", + score: 0.15956816751152955, + scoreBreakdown: map[int]float64{0: 0.04737179972998534, 2: 0.04737179972998534, 6: 0.04737179972998534, 7: 0.04737179972998534, 10: 0.04737179972998534, 12: 0.04737179972998534, 14: 0.04737179972998534, 15: 0.04737179972998534}, + }, + }, + }, + { + // trigger disjunction slice searcher (< 10 searchers) + // expect BLANK to give a 0 in score breakdown + query: `{"disjuncts":[{"term":"blank","field":"text"},{"term":"lorem","field":"text"},{"term":"ipsum","field":"text"},{"term":"blank","field":"text"},{"term":"blank","field":"text"},{"term":"dolor","field":"text"},{"term":"sit","field":"text"},{"term":"blank","field":"text"}]}`, + typ: "disjunction", + expectHits: []testResult{ + { + docID: "doc1", + score: 0.1340684440934241, + scoreBreakdown: map[int]float64{1: 0.05756326446708409, 2: 0.05756326446708409, 5: 0.09544709478559595, 6: 0.05756326446708409}, + }, + { + docID: "doc2", + score: 0.05179425287147191, + scoreBreakdown: map[int]float64{1: 0.0779410306721006, 5: 0.129235980813787}, + }, + { + docID: "doc3", + score: 0.0389705153360503, + scoreBreakdown: map[int]float64{2: 0.0779410306721006, 6: 0.0779410306721006}, + }, + { + docID: "doc4", + score: 0.07593627256602972, + scoreBreakdown: map[int]float64{1: 0.06749890894758198, 2: 0.06749890894758198, 6: 0.06749890894758198}, + }, + }, + }, + } + for _, dtq := range testQueries { + var q query.Query + var rv query.DisjunctionQuery + err := json.Unmarshal([]byte(dtq.query), &rv) + if err != nil { + t.Fatal(err) + } + rv.RetrieveScoreBreakdown(true) + q = &rv + sr := NewSearchRequest(q) + sr.SortBy([]string{"_id"}) + sr.Explain = true + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if len(res.Hits) != len(dtq.expectHits) { + t.Fatalf("expected %d hits, got %d", len(dtq.expectHits), len(res.Hits)) + } + for i, hit := range res.Hits { + if hit.ID != dtq.expectHits[i].docID { + t.Fatalf("expected docID %s, got %s", dtq.expectHits[i].docID, hit.ID) + } + if len(hit.ScoreBreakdown) != len(dtq.expectHits[i].scoreBreakdown) { + t.Fatalf("expected %d score breakdown, got %d", len(dtq.expectHits[i].scoreBreakdown), len(hit.ScoreBreakdown)) + } + for j, score := range hit.ScoreBreakdown { + actualScore := roundToDecimalPlace(score, 3) + expectScore := roundToDecimalPlace(dtq.expectHits[i].scoreBreakdown[j], 3) + if actualScore != expectScore { + t.Fatalf("expected score breakdown %f, got %f", dtq.expectHits[i].scoreBreakdown[j], score) + } + } + } + } +} + +func TestAutoFuzzy(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + imap := mapping.NewIndexMapping() + + if err := imap.AddCustomAnalyzer("splitter", map[string]interface{}{ + "type": custom.Name, + "tokenizer": whitespace.Name, + "token_filters": []interface{}{lowercase.Name}, + }); err != nil { + t.Fatal(err) + } + + textField := mapping.NewTextFieldMapping() + textField.Analyzer = "splitter" + textField.Store = true + textField.IncludeTermVectors = true + textField.IncludeInAll = true + + imap.DefaultMapping.Dynamic = false + imap.DefaultMapping.AddFieldMappingsAt("model", textField) + + documents := map[string]map[string]interface{}{ + "product1": { + "model": "apple iphone 12", + }, + "product2": { + "model": "apple iphone 13", + }, + "product3": { + "model": "samsung galaxy s22", + }, + "product4": { + "model": "samsung galaxy note", + }, + "product5": { + "model": "google pixel 5", + }, + "product6": { + "model": "oneplus 9 pro", + }, + "product7": { + "model": "xiaomi mi 11", + }, + "product8": { + "model": "oppo find x3", + }, + "product9": { + "model": "vivo x60 pro", + }, + "product10": { + "model": "oneplus 8t pro", + }, + "product11": { + "model": "nokia xr20", + }, + "product12": { + "model": "poco f1", + }, + "product13": { + "model": "asus rog 5", + }, + "product14": { + "model": "samsung galaxy a15 5g", + }, + "product15": { + "model": "tecno camon 17", + }, + } + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + type testStruct struct { + query string + expectHits []string + } + testQueries := []testStruct{ + { + // match query with fuzziness set to 2 + query: `{ + "match" : "applle iphone 12", + "fuzziness": 2, + "field" : "model" + }`, + expectHits: []string{"product1", "product2", "product7", "product14", "product15", "product12", "product10", "product3", "product6", "product8"}, + }, + { + // match query with fuzziness set to "auto" + query: `{ + "match" : "applle iphone 12", + "fuzziness": "auto", + "field" : "model" + }`, + expectHits: []string{"product1", "product2"}, + }, + { + // match query with fuzziness set to 2 with `and` operator + query: `{ + "match" : "applle iphone 12", + "fuzziness": 2, + "field" : "model", + "operator": "and" + }`, + expectHits: []string{"product1", "product2"}, + }, + { + // match query with fuzziness set to "auto" with `and`` operator + query: `{ + "match" : "applle iphone 12", + "fuzziness": "auto", + "field" : "model", + "operator": "and" + }`, + expectHits: []string{"product1"}, + }, + // match phrase query with fuzziness set to 2 + { + query: `{ + "match_phrase" : "onplus 9 pro", + "fuzziness": 2, + "field" : "model" + }`, + expectHits: []string{"product6", "product10"}, + }, + // match phrase query with fuzziness set to "auto" + { + query: `{ + "match_phrase" : "onplus 9 pro", + "fuzziness": "auto", + "field" : "model" + }`, + expectHits: []string{"product6"}, + }, + } + + for _, dtq := range testQueries { + q, err := query.ParseQuery([]byte(dtq.query)) + if err != nil { + t.Fatal(err) + } + + sr := NewSearchRequest(q) + sr.Highlight = NewHighlightWithStyle(ansi.Name) + sr.SortBy([]string{"-_score", "_id"}) + sr.Fields = []string{"*"} + sr.Explain = true + + res, err := idx.Search(sr) + if err != nil { + t.Fatal(err) + } + if len(res.Hits) != len(dtq.expectHits) { + t.Fatalf("expected %d hits, got %d", len(dtq.expectHits), len(res.Hits)) + } + for i, hit := range res.Hits { + if hit.ID != dtq.expectHits[i] { + t.Fatalf("expected docID %s, got %s", dtq.expectHits[i], hit.ID) + } + } + } +} + +func TestThesaurusTermReader(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + synonymCollection := "collection1" + + synonymSourceName := "english" + + analyzer := simple.Name + + synonymSourceConfig := map[string]interface{}{ + "collection": synonymCollection, + "analyzer": analyzer, + } + + textField := mapping.NewTextFieldMapping() + textField.Analyzer = analyzer + textField.SynonymSource = synonymSourceName + + imap := mapping.NewIndexMapping() + imap.DefaultMapping.AddFieldMappingsAt("text", textField) + err := imap.AddSynonymSource(synonymSourceName, synonymSourceConfig) + if err != nil { + t.Fatal(err) + } + err = imap.Validate() + if err != nil { + t.Fatal(err) + } + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + documents := map[string]map[string]interface{}{ + "doc1": { + "text": "quick brown fox eats", + }, + "doc2": { + "text": "fast red wolf jumps", + }, + "doc3": { + "text": "quick red cat runs", + }, + "doc4": { + "text": "speedy brown dog barks", + }, + "doc5": { + "text": "fast green rabbit hops", + }, + } + + batch := idx.NewBatch() + for docID, doc := range documents { + err := batch.Index(docID, doc) + if err != nil { + t.Fatal(err) + } + } + + synonymDocuments := map[string]*SynonymDefinition{ + "synDoc1": { + Synonyms: []string{"quick", "fast", "speedy"}, + }, + "synDoc2": { + Input: []string{"color", "colour"}, + Synonyms: []string{"red", "green", "blue", "yellow", "brown"}, + }, + "synDoc3": { + Input: []string{"animal", "creature"}, + Synonyms: []string{"fox", "wolf", "cat", "dog", "rabbit"}, + }, + "synDoc4": { + Synonyms: []string{"eats", "jumps", "runs", "barks", "hops"}, + }, + } + + for synName, synDef := range synonymDocuments { + err := batch.IndexSynonym(synName, synonymCollection, synDef) + if err != nil { + t.Fatal(err) + } + } + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + + sco, err := idx.Advanced() + if err != nil { + t.Fatal(err) + } + + reader, err := sco.Reader() + if err != nil { + t.Fatal(err) + } + defer func() { + err = reader.Close() + if err != nil { + t.Fatal(err) + } + }() + + thesReader, ok := reader.(index.ThesaurusReader) + if !ok { + t.Fatal("expected thesaurus reader") + } + + type testStruct struct { + queryTerm string + expectedSynonyms []string + } + + testQueries := []testStruct{ + { + queryTerm: "quick", + expectedSynonyms: []string{"fast", "speedy"}, + }, + { + queryTerm: "red", + expectedSynonyms: []string{}, + }, + { + queryTerm: "color", + expectedSynonyms: []string{"red", "green", "blue", "yellow", "brown"}, + }, + { + queryTerm: "colour", + expectedSynonyms: []string{"red", "green", "blue", "yellow", "brown"}, + }, + { + queryTerm: "animal", + expectedSynonyms: []string{"fox", "wolf", "cat", "dog", "rabbit"}, + }, + { + queryTerm: "creature", + expectedSynonyms: []string{"fox", "wolf", "cat", "dog", "rabbit"}, + }, + { + queryTerm: "fox", + expectedSynonyms: []string{}, + }, + { + queryTerm: "eats", + expectedSynonyms: []string{"jumps", "runs", "barks", "hops"}, + }, + { + queryTerm: "jumps", + expectedSynonyms: []string{"eats", "runs", "barks", "hops"}, + }, + } + + for _, test := range testQueries { + str, err := thesReader.ThesaurusTermReader(context.Background(), synonymSourceName, []byte(test.queryTerm)) + if err != nil { + t.Fatal(err) + } + var gotSynonyms []string + for { + synonym, err := str.Next() + if err != nil { + t.Fatal(err) + } + if synonym == "" { + break + } + gotSynonyms = append(gotSynonyms, string(synonym)) + } + if len(gotSynonyms) != len(test.expectedSynonyms) { + t.Fatalf("expected %d synonyms, got %d", len(test.expectedSynonyms), len(gotSynonyms)) + } + sort.Strings(gotSynonyms) + sort.Strings(test.expectedSynonyms) + for i, syn := range gotSynonyms { + if syn != test.expectedSynonyms[i] { + t.Fatalf("expected synonym %s, got %s", test.expectedSynonyms[i], syn) + } + } + } +} + +func TestSynonymSearchQueries(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + synonymCollection := "collection1" + + synonymSourceName := "english" + + analyzer := en.AnalyzerName + + synonymSourceConfig := map[string]interface{}{ + "collection": synonymCollection, + "analyzer": analyzer, + } + + textField := mapping.NewTextFieldMapping() + textField.Analyzer = analyzer + textField.SynonymSource = synonymSourceName + + imap := mapping.NewIndexMapping() + imap.DefaultMapping.AddFieldMappingsAt("text", textField) + err := imap.AddSynonymSource(synonymSourceName, synonymSourceConfig) + if err != nil { + t.Fatal(err) + } + err = imap.Validate() + if err != nil { + t.Fatal(err) + } + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + documents := map[string]map[string]interface{}{ + "doc1": { + "text": `The hardworking employee consistently strives to exceed expectations. + His industrious nature makes him a valuable asset to any team. + His conscientious attention to detail ensures that projects are completed efficiently and accurately. + He remains persistent even in the face of challenges.`, + }, + "doc2": { + "text": `The tranquil surroundings of the retreat provide a perfect escape from the hustle and bustle of city life. + Guests enjoy the peaceful atmosphere, which is perfect for relaxation and rejuvenation. + The calm environment offers the ideal place to meditate and connect with nature. + Even the most stressed individuals find themselves feeling relaxed and at ease.`, + }, + "doc3": { + "text": `The house was burned down, leaving only a charred shell behind. + The intense heat of the flames caused the walls to warp and the roof to cave in. + The seared remains of the furniture told the story of the blaze. + The incinerated remains left little more than ashes to remember what once was.`, + }, + "doc4": { + "text": `The faithful dog followed its owner everywhere, always loyal and steadfast. + It was devoted to protecting its family, and its reliable nature meant it could always be trusted. + In the face of danger, the dog remained calm, knowing its role was to stay vigilant. + Its trustworthy companionship provided comfort and security.`, + }, + "doc5": { + "text": `The lively market is bustling with activity from morning to night. + The dynamic energy of the crowd fills the air as vendors sell their wares. + Shoppers wander from stall to stall, captivated by the vibrant colors and energetic atmosphere. + This place is alive with movement and life.`, + }, + "doc6": { + "text": `In moments of crisis, bravery shines through. + It takes valor to step forward when others are afraid to act. + Heroes are defined by their guts and nerve, taking risks to protect others. + Boldness in the face of danger is what sets them apart.`, + }, + "doc7": { + "text": `Innovation is the driving force behind progress in every industry. + The company fosters an environment of invention, encouraging creativity at every level. + The focus on novelty and improvement means that ideas are always evolving. + The development of new solutions is at the core of the company's mission.`, + }, + "doc8": { + "text": `The blazing sunset cast a radiant glow over the horizon, painting the sky with hues of red and orange. + The intense heat of the day gave way to a fiery display of color. + As the sun set, the glowing light illuminated the landscape, creating a breathtaking scene. + The fiery sky was a sight to behold.`, + }, + "doc9": { + "text": `The fertile soil of the valley makes it perfect for farming. + The productive land yields abundant crops year after year. + Farmers rely on the rich, fruitful ground to sustain their livelihoods. + The area is known for its plentiful harvests, supporting both local communities and export markets.`, + }, + "doc10": { + "text": `The arid desert is a vast, dry expanse with little water or vegetation. + The barren landscape stretches as far as the eye can see, offering little respite from the scorching sun. + The desolate environment is unforgiving to those who venture too far without preparation. + The parched earth cracks under the heat, creating a harsh, unyielding terrain.`, + }, + "doc11": { + "text": `The fox is known for its cunning and intelligence. + As a predator, it relies on its sharp instincts to outwit its prey. + Its vulpine nature makes it both mysterious and fascinating. + The fox's ability to hunt with precision and stealth is what makes it such a formidable hunter.`, + }, + "doc12": { + "text": `The dog is often considered man's best friend due to its loyal nature. + As a companion, the hound provides both protection and affection. + The puppy quickly becomes a member of the family, always by your side. + Its playful energy and unshakable loyalty make it a beloved pet.`, + }, + "doc13": { + "text": `He worked tirelessly through the night, always persistent in his efforts. + His industrious approach to problem-solving kept the project moving forward. + No matter how difficult the task, he remained focused, always giving his best. + His dedication paid off when the project was completed ahead of schedule.`, + }, + "doc14": { + "text": `The river flowed calmly through the valley, its peaceful current offering a sense of tranquility. + Fishermen relaxed by the banks, enjoying the calm waters that reflected the sky above. + The tranquil nature of the river made it a perfect spot for meditation. + As the day ended, the river's quiet flow brought a sense of peace.`, + }, + "doc15": { + "text": `After the fire, all that was left was the charred remains of what once was. + The seared walls of the house told a tragic story. + The intensity of the blaze had burned everything in its path, leaving only the smoldering wreckage behind. + The incinerated objects could not be salvaged, and the damage was beyond repair.`, + }, + "doc16": { + "text": `The devoted employee always went above and beyond to complete his tasks. + His steadfast commitment to the company made him a valuable team member. + He was reliable, never failing to meet deadlines. + His trustworthiness earned him the respect of his colleagues, and was considered an + ingenious expert in his field.`, + }, + "doc17": { + "text": `The city is vibrant, full of life and energy. + The dynamic pace of the streets reflects the diverse culture of its inhabitants. + People from all walks of life contribute to the energetic atmosphere. + The city's lively spirit can be felt in every corner, from the bustling markets to the lively festivals.`, + }, + "doc18": { + "text": `In a moment of uncertainty, he made a bold decision that would change his life forever. + It took courage and nerve to take the leap, but his bravery paid off. + The guts to face the unknown allowed him to achieve something remarkable. + Being an bright scholar, the skill he demonstrated inspired those around him.`, + }, + "doc19": { + "text": `Innovation is often born from necessity, and the lightbulb is a prime example. + Thomas Edison's invention changed the world, offering a new way to see the night. + The creativity involved in developing such a groundbreaking product sparked a wave of + novelty in the scientific community. This improvement in technology continues to shape the modern world. + He was a clever academic and a smart researcher.`, + }, + "doc20": { + "text": `The fiery volcano erupted with a force that shook the earth. Its radiant lava flowed down the sides, + illuminating the night sky. The intense heat from the eruption could be felt miles away, as the + glowing lava burned everything in its path. The fiery display was both terrifying and mesmerizing.`, + }, + } + + synonymDocuments := map[string]*SynonymDefinition{ + "synDoc1": { + Synonyms: []string{"hardworking", "industrious", "conscientious", "persistent", "focused", "devoted"}, + }, + "synDoc2": { + Synonyms: []string{"tranquil", "peaceful", "calm", "relaxed", "unruffled"}, + }, + "synDoc3": { + Synonyms: []string{"burned", "charred", "seared", "incinerated", "singed"}, + }, + "synDoc4": { + Synonyms: []string{"faithful", "steadfast", "devoted", "reliable", "trustworthy"}, + }, + "synDoc5": { + Synonyms: []string{"lively", "dynamic", "energetic", "vivid", "vibrating"}, + }, + "synDoc6": { + Synonyms: []string{"bravery", "valor", "guts", "nerve", "boldness"}, + }, + "synDoc7": { + Input: []string{"innovation"}, + Synonyms: []string{"invention", "creativity", "novelty", "improvement", "development"}, + }, + "synDoc8": { + Input: []string{"blazing"}, + Synonyms: []string{"intense", "radiant", "burning", "fiery", "glowing"}, + }, + "synDoc9": { + Input: []string{"fertile"}, + Synonyms: []string{"productive", "fruitful", "rich", "abundant", "plentiful"}, + }, + "synDoc10": { + Input: []string{"arid"}, + Synonyms: []string{"dry", "barren", "desolate", "parched", "unfertile"}, + }, + "synDoc11": { + Input: []string{"fox"}, + Synonyms: []string{"vulpine", "canine", "predator", "hunter", "pursuer"}, + }, + "synDoc12": { + Input: []string{"dog"}, + Synonyms: []string{"canine", "hound", "puppy", "pup", "companion"}, + }, + "synDoc13": { + Synonyms: []string{"researcher", "scientist", "scholar", "academic", "expert"}, + }, + "synDoc14": { + Synonyms: []string{"bright", "clever", "ingenious", "sharp", "astute", "smart"}, + }, + } + + // Combine both maps into a slice of map entries (as they both have similar structure) + var combinedDocIDs []string + for id := range synonymDocuments { + combinedDocIDs = append(combinedDocIDs, id) + } + for id := range documents { + combinedDocIDs = append(combinedDocIDs, id) + } + rand.Shuffle(len(combinedDocIDs), func(i, j int) { + combinedDocIDs[i], combinedDocIDs[j] = combinedDocIDs[j], combinedDocIDs[i] + }) + + // Function to create batches of 5 + createDocBatches := func(docs []string, batchSize int) [][]string { + var batches [][]string + for i := 0; i < len(docs); i += batchSize { + end := i + batchSize + if end > len(docs) { + end = len(docs) + } + batches = append(batches, docs[i:end]) + } + return batches + } + // Create batches of 5 documents + batchSize := 5 + docBatches := createDocBatches(combinedDocIDs, batchSize) + if len(docBatches) == 0 { + t.Fatal("expected batches") + } + totalDocs := 0 + for _, batch := range docBatches { + totalDocs += len(batch) + } + if totalDocs != len(combinedDocIDs) { + t.Fatalf("expected %d documents, got %d", len(combinedDocIDs), totalDocs) + } + + var batches []*Batch + for _, docBatch := range docBatches { + batch := idx.NewBatch() + for _, docID := range docBatch { + if synDef, ok := synonymDocuments[docID]; ok { + err := batch.IndexSynonym(docID, synonymCollection, synDef) + if err != nil { + t.Fatal(err) + } + } else { + err := batch.Index(docID, documents[docID]) + if err != nil { + t.Fatal(err) + } + } + } + batches = append(batches, batch) + } + for _, batch := range batches { + err = idx.Batch(batch) + if err != nil { + t.Fatal(err) + } + } + + type testStruct struct { + query string + expectHits []string + } + + testQueries := []testStruct{ + { + query: `{ + "match": "hardworking employee", + "field": "text" + }`, + expectHits: []string{"doc1", "doc13", "doc16", "doc4", "doc7"}, + }, + { + query: `{ + "match": "Hardwork and industrius efforts bring lovely and tranqual moments, with a glazing blow of valour.", + "field": "text", + "fuzziness": "auto" + }`, + expectHits: []string{ + "doc1", "doc13", "doc14", "doc15", "doc16", + "doc17", "doc18", "doc2", "doc20", "doc3", + "doc4", "doc5", "doc6", "doc7", "doc8", "doc9", + }, + }, + { + query: `{ + "prefix": "in", + "field": "text" + }`, + expectHits: []string{ + "doc1", "doc11", "doc13", "doc15", "doc16", + "doc17", "doc18", "doc19", "doc2", "doc20", + "doc3", "doc4", "doc7", "doc8", + }, + }, + { + query: `{ + "prefix": "vivid", + "field": "text" + }`, + expectHits: []string{ + "doc17", "doc5", + }, + }, + { + query: `{ + "match_phrase": "smart academic", + "field": "text" + }`, + expectHits: []string{"doc16", "doc18", "doc19"}, + }, + { + query: `{ + "match_phrase": "smrat acedemic", + "field": "text", + "fuzziness": "auto" + }`, + expectHits: []string{"doc16", "doc18", "doc19"}, + }, + { + query: `{ + "wildcard": "br*", + "field": "text" + }`, + expectHits: []string{"doc11", "doc14", "doc16", "doc18", "doc19", "doc6", "doc8"}, + }, + } + + getTotalSynonymSearchStat := func(idx Index) int { + ir, err := idx.Advanced() + if err != nil { + t.Fatal(err) + } + stat := ir.StatsMap()["synonym_searches"].(uint64) + return int(stat) + } + + runTestQueries := func(idx Index) error { + for _, dtq := range testQueries { + q, err := query.ParseQuery([]byte(dtq.query)) + if err != nil { + return err + } + sr := NewSearchRequest(q) + sr.Highlight = NewHighlightWithStyle(ansi.Name) + sr.SortBy([]string{"_id"}) + sr.Fields = []string{"*"} + sr.Size = 30 + sr.Explain = true + res, err := idx.Search(sr) + if err != nil { + return err + } + if len(res.Hits) != len(dtq.expectHits) { + return fmt.Errorf("expected %d hits, got %d", len(dtq.expectHits), len(res.Hits)) + } + // sort the expected hits to match the order of the search results + sort.Strings(dtq.expectHits) + for i, hit := range res.Hits { + if hit.ID != dtq.expectHits[i] { + return fmt.Errorf("expected docID %s, got %s", dtq.expectHits[i], hit.ID) + } + } + } + return nil + } + err = runTestQueries(idx) + if err != nil { + t.Fatal(err) + } + // now verify that the stat for number of synonym enabled queries is correct + totalSynonymSearchStat := getTotalSynonymSearchStat(idx) + if totalSynonymSearchStat != len(testQueries) { + t.Fatalf("expected %d synonym searches, got %d", len(testQueries), totalSynonymSearchStat) + } + + // test with index alias - with 1 batch per index + numIndexes := len(batches) + indexes := make([]Index, numIndexes) + indexesPath := make([]string, numIndexes) + for i := 0; i < numIndexes; i++ { + tmpIndexPath := createTmpIndexPath(t) + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + err = idx.Batch(batches[i]) + if err != nil { + t.Fatal(err) + } + indexes[i] = idx + indexesPath[i] = tmpIndexPath + } + defer func() { + for i := 0; i < numIndexes; i++ { + err = indexes[i].Close() + if err != nil { + t.Fatal(err) + } + + cleanupTmpIndexPath(t, indexesPath[i]) + } + }() + alias := NewIndexAlias(indexes...) + + if err := alias.SetIndexMapping(imap); err != nil { + t.Fatal(err) + } + + err = runTestQueries(alias) + if err != nil { + t.Fatal(err) + } + // verify the synonym search stat for the alias + totalSynonymSearchStat = getTotalSynonymSearchStat(indexes[0]) + if totalSynonymSearchStat != len(testQueries) { + t.Fatalf("expected %d synonym searches, got %d", len(testQueries), totalSynonymSearchStat) + } + for i := 1; i < numIndexes; i++ { + idxStat := getTotalSynonymSearchStat(indexes[i]) + if idxStat != totalSynonymSearchStat { + t.Fatalf("expected %d synonym searches, got %d", totalSynonymSearchStat, idxStat) + } + } + if totalSynonymSearchStat != len(testQueries) { + t.Fatalf("expected %d synonym searches, got %d", len(testQueries), totalSynonymSearchStat) + } + // test with multi-level alias now with two index per alias + // and having any extra index being in the final alias + numAliases := numIndexes / 2 + extraIndex := numIndexes % 2 + aliases := make([]IndexAlias, numAliases) + for i := 0; i < numAliases; i++ { + alias := NewIndexAlias(indexes[i*2], indexes[i*2+1]) + aliases[i] = alias + } + if extraIndex > 0 { + aliases[numAliases-1].Add(indexes[numIndexes-1]) + } + alias = NewIndexAlias() + + if err := alias.SetIndexMapping(imap); err != nil { + t.Fatal(err) + } + + for i := 0; i < numAliases; i++ { + alias.Add(aliases[i]) + } + err = runTestQueries(alias) + if err != nil { + t.Fatal(err) + } + // verify the synonym searches stat for the alias + totalSynonymSearchStat = getTotalSynonymSearchStat(indexes[0]) + if totalSynonymSearchStat != 2*len(testQueries) { + t.Fatalf("expected %d synonym searches, got %d", len(testQueries), totalSynonymSearchStat) + } + for i := 1; i < numIndexes; i++ { + idxStat := getTotalSynonymSearchStat(indexes[i]) + if idxStat != totalSynonymSearchStat { + t.Fatalf("expected %d synonym searches, got %d", totalSynonymSearchStat, idxStat) + } + } +} + +func TestGeoDistanceInSort(t *testing.T) { + tmpIndexPath := createTmpIndexPath(t) + defer cleanupTmpIndexPath(t, tmpIndexPath) + + fm := mapping.NewGeoPointFieldMapping() + imap := mapping.NewIndexMapping() + imap.DefaultMapping.AddFieldMappingsAt("geo", fm) + + idx, err := New(tmpIndexPath, imap) + if err != nil { + t.Fatal(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Fatal(err) + } + }() + + qp := []float64{0, 0} + + docs := []struct { + id string + point []float64 + distance float64 + }{ + { + id: "1", + point: []float64{1, 1}, + distance: geo.Haversin(1, 1, qp[0], qp[1]) * 1000, + }, + { + id: "2", + point: []float64{2, 2}, + distance: geo.Haversin(2, 2, qp[0], qp[1]) * 1000, + }, + { + id: "3", + point: []float64{3, 3}, + distance: geo.Haversin(3, 3, qp[0], qp[1]) * 1000, + }, + } + + for _, doc := range docs { + if err := idx.Index(doc.id, map[string]interface{}{"geo": doc.point}); err != nil { + t.Fatal(err) + } + } + + q := NewGeoDistanceQuery(qp[0], qp[1], "1000000m") + q.SetField("geo") + req := NewSearchRequest(q) + req.Sort = make(search.SortOrder, 0) + req.Sort = append(req.Sort, &search.SortGeoDistance{ + Field: "geo", + Desc: false, + Unit: "m", + Lon: qp[0], + Lat: qp[1], + }) + res, err := idx.Search(req) + if err != nil { + t.Fatal(err) + } + + for i, doc := range res.Hits { + hitDist, err := strconv.ParseFloat(doc.Sort[0], 64) + if err != nil { + t.Fatal(err) + } + if math.Abs(hitDist-docs[i].distance) > 1 { + t.Fatalf("distance error greater than 1 meter, expected distance - %v, got - %v", docs[i].distance, hitDist) + } + } +} diff --git a/size/sizes.go b/size/sizes.go new file mode 100644 index 0000000..0990bf8 --- /dev/null +++ b/size/sizes.go @@ -0,0 +1,59 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package size + +import ( + "reflect" +) + +func init() { + var b bool + SizeOfBool = int(reflect.TypeOf(b).Size()) + var f32 float32 + SizeOfFloat32 = int(reflect.TypeOf(f32).Size()) + var f64 float64 + SizeOfFloat64 = int(reflect.TypeOf(f64).Size()) + var i int + SizeOfInt = int(reflect.TypeOf(i).Size()) + var m map[int]int + SizeOfMap = int(reflect.TypeOf(m).Size()) + var ptr *int + SizeOfPtr = int(reflect.TypeOf(ptr).Size()) + var slice []int + SizeOfSlice = int(reflect.TypeOf(slice).Size()) + var str string + SizeOfString = int(reflect.TypeOf(str).Size()) + var u8 uint8 + SizeOfUint8 = int(reflect.TypeOf(u8).Size()) + var u16 uint16 + SizeOfUint16 = int(reflect.TypeOf(u16).Size()) + var u32 uint32 + SizeOfUint32 = int(reflect.TypeOf(u32).Size()) + var u64 uint64 + SizeOfUint64 = int(reflect.TypeOf(u64).Size()) +} + +var SizeOfBool int +var SizeOfFloat32 int +var SizeOfFloat64 int +var SizeOfInt int +var SizeOfMap int +var SizeOfPtr int +var SizeOfSlice int +var SizeOfString int +var SizeOfUint8 int +var SizeOfUint16 int +var SizeOfUint32 int +var SizeOfUint64 int diff --git a/test/integration.go b/test/integration.go new file mode 100644 index 0000000..c45286b --- /dev/null +++ b/test/integration.go @@ -0,0 +1,27 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "github.com/blevesearch/bleve/v2" +) + +type SearchTest struct { + Search *bleve.SearchRequest `json:"search"` + Result *bleve.SearchResult `json:"result"` + Comment string `json:"comment"` +} + +type SearchTests []*SearchTest diff --git a/test/integration_test.go b/test/integration_test.go new file mode 100644 index 0000000..f2605b2 --- /dev/null +++ b/test/integration_test.go @@ -0,0 +1,279 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "flag" + "fmt" + "math" + "os" + "path/filepath" + "reflect" + "regexp" + "testing" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/mapping" + + // allow choosing alternate kvstores + _ "github.com/blevesearch/bleve/v2/config" +) + +var dataset = flag.String("dataset", "", "only test datasets matching this regex") +var onlynum = flag.Int("testnum", -1, "only run the test with this number") +var keepIndex = flag.Bool("keepIndex", false, "keep the index after testing") + +var indexType = flag.String("indexType", bleve.Config.DefaultIndexType, "index type to build") +var kvType = flag.String("kvType", bleve.Config.DefaultKVStore, "kv store type to build") +var segType = flag.String("segType", "", "force scorch segment type") +var segVer = flag.Int("segVer", 0, "force scorch segment version") + +func TestIntegration(t *testing.T) { + + flag.Parse() + + t.Logf("using index type %s and kv type %s", *indexType, *kvType) + if *segType != "" { + t.Logf("forcing segment type: %s", *segType) + } + if *segVer != 0 { + t.Logf("forcing segment version: %d", *segVer) + } + + var err error + var datasetRegexp *regexp.Regexp + if *dataset != "" { + datasetRegexp, err = regexp.Compile(*dataset) + if err != nil { + t.Fatal(err) + } + } + + entries, err := os.ReadDir("tests") + if err != nil { + t.Fatal(err) + } + for _, f := range entries { + if datasetRegexp != nil { + if !datasetRegexp.MatchString(f.Name()) { + continue + } + } + if f.IsDir() { + t.Logf("Running test: %s", f.Name()) + runTestDir(t, "tests"+string(filepath.Separator)+f.Name(), f.Name()) + } + } +} + +func runTestDir(t *testing.T, dir, datasetName string) { + // read the mapping + mappingBytes, err := os.ReadFile(dir + string(filepath.Separator) + "mapping.json") + if err != nil { + t.Errorf("error reading mapping: %v", err) + return + } + var mapping mapping.IndexMappingImpl + err = json.Unmarshal(mappingBytes, &mapping) + if err != nil { + t.Errorf("error unmarshalling mapping: %v", err) + return + } + + var index bleve.Index + var cleanup func() + + // if there is a dir named 'data' open single index + _, err = os.Stat(dir + string(filepath.Separator) + "data") + if !os.IsNotExist(err) { + + index, cleanup, err = loadDataSet(t, datasetName, mapping, dir+string(filepath.Separator)+"data") + if err != nil { + t.Errorf("error loading dataset: %v", err) + return + } + defer cleanup() + } else { + // if there is a dir named 'datasets' build alias over each index + _, err = os.Stat(dir + string(filepath.Separator) + "datasets") + if !os.IsNotExist(err) { + index, cleanup, err = loadDataSets(t, datasetName, mapping, dir+string(filepath.Separator)+"datasets") + if err != nil { + t.Errorf("error loading dataset: %v", err) + return + } + defer cleanup() + } + } + + // read the searches + searchBytes, err := os.ReadFile(dir + string(filepath.Separator) + "searches.json") + if err != nil { + t.Errorf("error reading searches: %v", err) + return + } + var searches SearchTests + err = json.Unmarshal(searchBytes, &searches) + if err != nil { + t.Errorf("error unmarshalling searches: %v", err) + return + } + + // run the searches + for testNum, search := range searches { + if *onlynum < 0 || (*onlynum > 0 && testNum == *onlynum) { + res, err := index.Search(search.Search) + if err != nil { + t.Errorf("error running search: %v", err) + } + if res.Total != search.Result.Total { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - expected total: %d got %d", testNum, search.Result.Total, res.Total) + continue + } + if len(res.Hits) != len(search.Result.Hits) { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - expected hits len: %d got %d", testNum, len(search.Result.Hits), len(res.Hits)) + t.Errorf("got hits: %v", res.Hits) + continue + } + for hi, hit := range search.Result.Hits { + if hit.ID != res.Hits[hi].ID { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - expected hit %d to have ID %s got %s", testNum, hi, hit.ID, res.Hits[hi].ID) + } + if hit.Fields != nil { + if !reflect.DeepEqual(hit.Fields, res.Hits[hi].Fields) { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - expected hit %d to have fields %#v got %#v", testNum, hi, hit.Fields, res.Hits[hi].Fields) + } + } + if hit.Fragments != nil { + if !reflect.DeepEqual(hit.Fragments, res.Hits[hi].Fragments) { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - expected hit %d to have fragments %#v got %#v", testNum, hi, hit.Fragments, res.Hits[hi].Fragments) + } + } + if hit.Locations != nil { + if !reflect.DeepEqual(hit.Locations, res.Hits[hi].Locations) { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - expected hit %d to have locations %#v got %#v", testNum, hi, hit.Locations, res.Hits[hi].Locations) + } + } + // assert that none of the scores were NaN,+Inf,-Inf + if math.IsInf(res.Hits[hi].Score, 0) || math.IsNaN(res.Hits[hi].Score) { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - invalid score %f", testNum, res.Hits[hi].Score) + } + } + if search.Result.Facets != nil { + if !reflect.DeepEqual(search.Result.Facets, res.Facets) { + t.Errorf("test error - %s", search.Comment) + t.Errorf("test %d - expected facets: %#v got %#v", testNum, search.Result.Facets, res.Facets) + } + } + if _, ok := index.(bleve.IndexAlias); !ok { + // check that custom index name is in results + for _, hit := range res.Hits { + if hit.Index != datasetName { + t.Fatalf("expected name: %s, got: %s", datasetName, hit.Index) + } + } + } + } + } +} + +func loadDataSet(t *testing.T, datasetName string, mapping mapping.IndexMappingImpl, path string) (bleve.Index, func(), error) { + idxPath := fmt.Sprintf("test-%s.bleve", datasetName) + cfg := map[string]interface{}{} + if *segType != "" { + cfg["forceSegmentType"] = *segType + } + if *segVer != 0 { + cfg["forceSegmentVersion"] = *segVer + } + + index, err := bleve.NewUsing(idxPath, &mapping, *indexType, *kvType, cfg) + if err != nil { + return nil, nil, fmt.Errorf("error creating new index: %v", err) + } + // set a custom index name + index.SetName(datasetName) + + // index data + entries, err := os.ReadDir(path) + if err != nil { + return nil, nil, fmt.Errorf("error reading data dir: %v", err) + } + for _, f := range entries { + fileBytes, err := os.ReadFile(path + string(filepath.Separator) + f.Name()) + if err != nil { + return nil, nil, fmt.Errorf("error reading data file: %v", err) + } + var fileDoc interface{} + err = json.Unmarshal(fileBytes, &fileDoc) + if err != nil { + return nil, nil, fmt.Errorf("error parsing data file as json: %v", err) + } + filename := f.Name() + ext := filepath.Ext(filename) + id := filename[0 : len(filename)-len(ext)] + err = index.Index(id, fileDoc) + if err != nil { + return nil, nil, fmt.Errorf("error indexing data: %v", err) + } + } + cleanup := func() { + err := index.Close() + if err != nil { + t.Fatalf("error closing index: %v", err) + } + if !*keepIndex { + err := os.RemoveAll(idxPath) + if err != nil { + t.Fatalf("error removing index: %v", err) + } + } + } + return index, cleanup, nil +} + +func loadDataSets(t *testing.T, datasetName string, mapping mapping.IndexMappingImpl, path string) (bleve.Index, func(), error) { + entries, err := os.ReadDir(path) + if err != nil { + return nil, nil, fmt.Errorf("error reading datasets dir: %v", err) + } + var cleanups []func() + alias := bleve.NewIndexAlias() + for _, f := range entries { + idx, idxCleanup, err := loadDataSet(t, f.Name(), mapping, path+string(filepath.Separator)+f.Name()) + if err != nil { + return nil, nil, fmt.Errorf("error loading dataset: %v", err) + } + cleanups = append(cleanups, idxCleanup) + alias.Add(idx) + } + alias.SetName(datasetName) + + cleanupAll := func() { + for _, cleanup := range cleanups { + cleanup() + } + } + + return alias, cleanupAll, nil +} diff --git a/test/ip_field_test.go b/test/ip_field_test.go new file mode 100644 index 0000000..04f9762 --- /dev/null +++ b/test/ip_field_test.go @@ -0,0 +1,272 @@ +// Copyright (c) 2021 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "net" + "testing" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/mapping" +) + +type doc struct { + IP string `json:"ip"` +} + +func createIdx(t *testing.T) bleve.Index { + ipIndexed := mapping.NewIPFieldMapping() + ipIndexed.Name = "ip" + + lineMapping := bleve.NewDocumentStaticMapping() + lineMapping.AddFieldMappingsAt("ip", ipIndexed) + + mapping := bleve.NewIndexMapping() + mapping.DefaultMapping = lineMapping + mapping.DefaultAnalyzer = "standard" + + idx, err := bleve.NewMemOnly(mapping) + if err != nil { + t.Fatal(err) + } + return idx +} + +func Test_ipv4CidrQuery(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + err := idx.Index("id1", doc{"192.168.1.21"}) + if err != nil { + t.Fatal(err) + } + + reqStr := `192.168.1.0/24` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + search := bleve.NewSearchRequest(query) + res, err := idx.Search(search) + if err != nil { + t.Fatal(err) + } + + if res.Total != 1 { + t.Fatalf("failed to find %q, res -> %s", reqStr, res) + } + if res.Hits[0].ID != "id1" { + t.Fatalf("expected %q got %q", "id1", res.Hits[0].Index) + } +} + +func Test_ipv6CidrQuery(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + err := idx.Index("id1", doc{"2a00:23c8:7283:ff00:1fa8:2af6:9dec:6b19"}) + if err != nil { + t.Fatal(err) + } + + reqStr := `2a00:23c8:7283:ff00:1fa8:0:0:0/80` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + search := bleve.NewSearchRequest(query) + res, err := idx.Search(search) + if err != nil { + t.Fatal(err) + } + + if res.Total != 1 { + t.Fatalf("failed to find %q, res -> %s", reqStr, res) + } + if res.Hits[0].ID != "id1" { + t.Fatalf("expected %q got %q", "id1", res.Hits[0].Index) + } +} + +func Test_MultiIPvr4CidrQuery(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + err := idx.Index("id1", doc{"192.168.1.0"}) + if err != nil { + t.Fatal(err) + } + err = idx.Index("id2", doc{"192.168.1.255"}) + if err != nil { + t.Fatal(err) + } + err = idx.Index("id3", doc{"192.168.2.22"}) + if err != nil { + t.Fatal(err) + } + + reqStr := `192.168.1.0/24` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + search := bleve.NewSearchRequest(query) + res, err := idx.Search(search) + if err != nil { + t.Fatal(err) + } + + if res.Total != 2 { + t.Fatalf("failed to find %q, res -> %s", reqStr, res) + } + if res.Hits[0].ID != "id1" { + t.Fatalf("expected %q got %q", "id1", res.Hits[0].ID) + } + if res.Hits[1].ID != "id2" { + t.Fatalf("expected %q got %q", "id2", res.Hits[0].Index) + } +} + +func Test_CidrQueryNonDivisibleBy8(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + err := idx.Index("id1", doc{"192.168.1.1"}) + if err != nil { + t.Fatal(err) + } + err = idx.Index("id2", doc{"192.168.1.2"}) + if err != nil { + t.Fatal(err) + } + err = idx.Index("id3", doc{"192.168.2.5"}) + if err != nil { + t.Fatal(err) + } + err = idx.Index("id4", doc{"192.168.2.6"}) + if err != nil { + t.Fatal(err) + } + + reqStr := `192.168.1.0/30` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + search := bleve.NewSearchRequest(query) + res, err := idx.Search(search) + if err != nil { + t.Fatal(err) + } + + if res.Total != 2 { + t.Fatalf("failed to find %q, res -> %s", reqStr, res) + } + if res.Hits[0].ID != "id1" { + t.Fatalf("expected %q got %q", "id1", res.Hits[0].ID) + } + if res.Hits[1].ID != "id2" { + t.Fatalf("expected %q got %q", "id2", res.Hits[0].Index) + } +} + +func Test_simpleIPv4MatchQuery(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + err := idx.Index("id1", doc{"192.168.1.21"}) + if err != nil { + t.Fatal(err) + } + + reqStr := `192.168.1.21` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + search := bleve.NewSearchRequest(query) + res, err := idx.Search(search) + if err != nil { + t.Fatal(err) + } + + if res.Total != 1 { + t.Fatalf("failed to find %q, res -> %s", reqStr, res) + } + if res.Hits[0].ID != "id1" { + t.Fatalf("expected %q got %q", "id1", res.Hits[0].Index) + } +} + +func Test_ipv4LiteralData(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + type stronglyTyped struct { + IP net.IP `json:"ip"` + } + + err := idx.Index("id1", stronglyTyped{net.ParseIP("192.168.1.21")}) + if err != nil { + t.Fatal(err) + } + + reqStr := `192.168.1.0/24` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + search := bleve.NewSearchRequest(query) + res, err := idx.Search(search) + if err != nil { + t.Fatal(err) + } + + if res.Total != 1 { + t.Fatalf("failed to find %q, res -> %s", reqStr, res) + } + if res.Hits[0].ID != "id1" { + t.Fatalf("expected %q got %q", "id1", res.Hits[0].Index) + } +} + +func Test_badIPFmt(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + reqStr := `192.168.1.` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + search := bleve.NewSearchRequest(query) + _, err := idx.Search(search) + if err == nil { + t.Errorf("%q is not a valid IP", reqStr) + } +} + +func Test_badCIDRFmt(t *testing.T) { + idx := createIdx(t) + defer idx.Close() + + reqStr := `/` + query := bleve.NewIPRangeQuery(reqStr) + query.FieldVal = "ip" + + err := query.Validate() + if err == nil { + t.Errorf("%q is not a valid CIDR", reqStr) + } + + search := bleve.NewSearchRequest(query) + _, err = idx.Search(search) + if err == nil { + t.Errorf("%q is not a valid CIDR", reqStr) + } +} diff --git a/test/knn/knn_dataset_queries.zip b/test/knn/knn_dataset_queries.zip new file mode 100644 index 0000000000000000000000000000000000000000..d840ded2f4ce2658ca1224e9f712f13f96252cac GIT binary patch literal 149642 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%0Dx1b1q7h28+7}(YHvuZTwvN1!WRuyxsOn`CdSP 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%0DtC2y13XVp8S}mGc^g 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 literal 0 HcmV?d00001 diff --git a/test/tests/alias/datasets/shard0/a.json b/test/tests/alias/datasets/shard0/a.json new file mode 100644 index 0000000..1aeaf2c --- /dev/null +++ b/test/tests/alias/datasets/shard0/a.json @@ -0,0 +1,3 @@ +{ + "name": "a" +} \ No newline at end of file diff --git a/test/tests/alias/datasets/shard0/c.json b/test/tests/alias/datasets/shard0/c.json new file mode 100644 index 0000000..a584a54 --- /dev/null +++ b/test/tests/alias/datasets/shard0/c.json @@ -0,0 +1,3 @@ +{ + "name": "c" +} \ No newline at end of file diff --git a/test/tests/alias/datasets/shard1/b.json b/test/tests/alias/datasets/shard1/b.json new file mode 100644 index 0000000..1c67e0d --- /dev/null +++ b/test/tests/alias/datasets/shard1/b.json @@ -0,0 +1,3 @@ +{ + "name": "b" +} \ No newline at end of file diff --git a/test/tests/alias/datasets/shard1/d.json b/test/tests/alias/datasets/shard1/d.json new file mode 100644 index 0000000..0a478db --- /dev/null +++ b/test/tests/alias/datasets/shard1/d.json @@ -0,0 +1,3 @@ +{ + "name": "d" +} \ No newline at end of file diff --git a/test/tests/alias/mapping.json b/test/tests/alias/mapping.json new file mode 100644 index 0000000..7f9bae7 --- /dev/null +++ b/test/tests/alias/mapping.json @@ -0,0 +1,3 @@ +{ + "default_analyzer": "keyword" +} diff --git a/test/tests/alias/searches.json b/test/tests/alias/searches.json new file mode 100644 index 0000000..fb8dd06 --- /dev/null +++ b/test/tests/alias/searches.json @@ -0,0 +1,76 @@ +[ + { + "comment": "match all across shards", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "match_all": {} + } + }, + "result": { + "total_hits": 4, + "hits": [ + { + "id": "a" + }, + { + "id": "b" + }, + { + "id": "c" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "search after b (page 2 when size=2)", + "search": { + "from": 0, + "size": 2, + "sort": ["name"], + "search_after": ["b"], + "query": { + "match_all": {} + } + }, + "result": { + "total_hits": 4, + "hits": [ + { + "id": "c" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "search before c (page 1 when size=2)", + "search": { + "from": 0, + "size": 2, + "sort": ["name"], + "search_before": ["c"], + "query": { + "match_all": {} + } + }, + "result": { + "total_hits": 4, + "hits": [ + { + "id": "a" + }, + { + "id": "b" + } + ] + } + } +] \ No newline at end of file diff --git a/test/tests/basic/data/a.json b/test/tests/basic/data/a.json new file mode 100644 index 0000000..992bbd9 --- /dev/null +++ b/test/tests/basic/data/a.json @@ -0,0 +1,7 @@ +{ + "id": "a", + "name": "marty", + "age": 19, + "title": "mista", + "tags": ["gopher", "belieber"] +} \ No newline at end of file diff --git a/test/tests/basic/data/b.json b/test/tests/basic/data/b.json new file mode 100644 index 0000000..0697272 --- /dev/null +++ b/test/tests/basic/data/b.json @@ -0,0 +1,7 @@ +{ + "id": "b", + "name": "steve has long & complicated name", + "age": 27, + "birthday": "2001-09-09T01:46:40Z", + "title": "missess" +} \ No newline at end of file diff --git a/test/tests/basic/data/c.json b/test/tests/basic/data/c.json new file mode 100644 index 0000000..2b5d308 --- /dev/null +++ b/test/tests/basic/data/c.json @@ -0,0 +1,7 @@ +{ + "id": "c", + "name": "bob walks home", + "age": 64, + "birthday": "2014-05-13T16:53:20Z", + "title": "masta" +} \ No newline at end of file diff --git a/test/tests/basic/data/d.json b/test/tests/basic/data/d.json new file mode 100644 index 0000000..dbc2f51 --- /dev/null +++ b/test/tests/basic/data/d.json @@ -0,0 +1,7 @@ +{ + "id": "d", + "name": "bobbleheaded wings top the phone", + "age": 72, + "birthday": "2014-05-13T16:53:20Z", + "title": "mizz" +} \ No newline at end of file diff --git a/test/tests/basic/mapping.json b/test/tests/basic/mapping.json new file mode 100644 index 0000000..a7a049b --- /dev/null +++ b/test/tests/basic/mapping.json @@ -0,0 +1,27 @@ +{ + "types": { + "person": { + "properties": { + "name": { + "fields": [ + { + "include_term_vectors": true, + "include_in_all": true, + "index": true, + "store": true, + "analyzer": "en", + "type": "text" + } + ], + "dynamic": true, + "enabled": true + }, + "id": { + "dynamic": false, + "enabled": false + } + } + } + }, + "default_type": "person" +} \ No newline at end of file diff --git a/test/tests/basic/searches.json b/test/tests/basic/searches.json new file mode 100644 index 0000000..7ddfce3 --- /dev/null +++ b/test/tests/basic/searches.json @@ -0,0 +1,883 @@ +[ + { + "comment": "test term search, exact match", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "term": "marti" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "test term search, no match", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "term": "noone" + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "test match phrase search", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "match_phrase": "steve has" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b" + } + ] + } + }, + { + "comment": "test term search, no match", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "term": "walking" + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "test match search, matching due to analysis", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "fuzziness": 0, + "prefix_length": 0, + "field": "name", + "match": "walking" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "c" + } + ] + } + }, + { + "comment": "test term prefix search", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "prefix": "bobble" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "d" + } + ] + } + }, + { + "comment": "test simple query string", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "query": "+name:phone" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "d" + } + ] + } + }, + { + "comment": "test numeric range, no lower bound", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "age", + "max": 30 + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "a" + }, + { + "id": "b" + } + ] + } + }, + { + "comment": "test numeric range, upper and lower bounds", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "age", + "max": 30, + "min": 20 + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b" + } + ] + } + }, + { + "comment": "test conjunction of numeric range, upper and lower bounds", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "conjuncts": [ + { + "boost": 1, + "field": "age", + "min": 20 + }, + { + "boost": 1, + "field": "age", + "max": 30 + } + ] + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b" + } + ] + } + }, + { + "comment": "test date range, no upper bound", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "birthday", + "start": "2010-01-01" + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "c" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "test numeric range, no lower bound", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "birthday", + "end": "2010-01-01" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b" + } + ] + } + }, + { + "comment": "test term search, matching inside an array", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "tags", + "term": "gopher" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "test term search, matching another element inside array", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "tags", + "term": "belieber" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "test term search, not present in array", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "tags", + "term": "notintagsarray" + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "with size 0, total should be 1, but hits empty", + "search": { + "from": 0, + "size": 0, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "term": "marti" + } + }, + "result": { + "total_hits": 1, + "hits": [] + } + }, + { + "comment": "a search for doc a that includes tags field, verifies both values come back", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "fields": ["tags"], + "query": { + "field": "name", + "term": "marti" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a", + "fields": { + "tags": ["gopher", "belieber"] + } + } + ] + } + }, + { + "comment": "test fuzzy search, fuzziness 1 with match", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "term": "msrti", + "fuzziness": 1 + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "highlight results", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "match": "long" + }, + "highlight": { + "fields": ["name"] + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b", + "fragments": { + "name": ["steve has <a> long & complicated name"] + } + } + ] + } + }, + { + "comment": "highlight results without specifying fields", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "match": "long" + }, + "highlight": {} + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b", + "fragments": { + "name": ["steve has <a> long & complicated name"] + } + } + ] + } + }, + { + "comment": "request fields", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "fields": ["age","birthday"], + "query": { + "field": "name", + "match": "long" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b", + "fields": { + "age": 27, + "birthday": "2001-09-09T01:46:40Z" + } + } + ] + } + }, + { + "comment": "tests query string only containing MUST NOT clause, bug #193", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "query": "-title:mista" + } + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "b" + }, + { + "id": "c" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "highlight results including non-matching field (which should be produced in its entirety, though unhighlighted)", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "match": "long" + }, + "highlight": { + "fields": ["name", "title"] + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b", + "fragments": { + "name": ["steve has <a> long & complicated name"], + "title": ["missess"] + } + } + ] + } + }, + { + "comment": "search and highlight an array field", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "tags", + "match": "gopher" + }, + "highlight": { + "fields": ["tags"] + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a", + "fragments": { + "tags": ["gopher"] + } + } + ] + } + }, + { + "comment": "reproduce bug in prefix search", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "title", + "prefix": "miss" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b" + } + ] + } + }, + { + "comment": "test match none", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "match_none": {} + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "test match all", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "match_all": {} + } + }, + "result": { + "total_hits": 4, + "hits": [ + { + "id": "a" + }, + { + "id": "b" + }, + { + "id": "c" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "test doc id query", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "ids": ["b", "c"] + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "b" + }, + { + "id": "c" + } + ] + } + }, + { + "comment": "test query string MUST and SHOULD", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "query": "+age:>20 missess" + } + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "b" + }, + { + "id": "c" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "test regexp matching term", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "regexp": "mar.*" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "test regexp that should not match when properly anchored", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "regexp": "mar." + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "test wildcard matching term", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "name", + "wildcard": "mar*" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "test boost - term query", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "disjuncts": [ + { + "field": "name", + "term": "marti", + "boost": 1.0 + }, + { + "field": "name", + "term": "steve", + "boost": 5.0 + } + ] + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "b" + }, + { + "id": "a" + } + ] + } + }, + { + "comment": "test boost - term query", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "disjuncts": [ + { + "field": "name", + "term": "marti", + "boost": 1.0 + }, + { + "fuzziness": 1, + "field": "name", + "term": "steve", + "boost": 5.0 + } + ] + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "b" + }, + { + "id": "a" + } + ] + } + }, + { + "comment": "test boost - numeric range query", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "disjuncts": [ + { + "field": "name", + "term": "marti", + "boost": 1.0 + }, + { + "field": "age", + "min": 25, + "max": 29, + "boost": 50.0 + } + ] + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "b" + }, + { + "id": "a" + } + ] + } + }, + { + "comment": "test boost - regexp query", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "disjuncts": [ + { + "field": "name", + "term": "marti", + "boost": 1.0 + }, + { + "field": "name", + "regexp": "stev.*", + "boost": 5.0 + } + ] + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "b" + }, + { + "id": "a" + } + ] + } + }, + { + "comment": "test wildcard inside query string", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "query": "name:mar*" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "test regexp inside query string", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "query": "name:/mar.*/" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "comment": "test term range", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "title", + "max": "miz", + "min": "mis" + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "a" + }, + { + "id": "b" + } + ] + } + } +] diff --git a/test/tests/employee/data/emp10508560.json b/test/tests/employee/data/emp10508560.json new file mode 100644 index 0000000..b8f7548 --- /dev/null +++ b/test/tests/employee/data/emp10508560.json @@ -0,0 +1,44 @@ +{ + "salary": 104561.8, + "_type": "emp", + "name": "Deirdre Reed", + "mutated": 0, + "is_manager": true, + "dept": "Accounts", + "join_date": "2003-05-28T21:29:00", + "manages": { + "team_size": 9, + "reports": [ + "Gallia Julián", + "Duvessa Nicolás", + "Beryl Thomas", + "Deirdre Julián", + "Antonia Gerónimo", + "Ciara Young", + "Riona Richardson IX", + "Severin Jr.", + "Perdita Morgan" + ] + }, + "languages_known": [ + "English", + "Spanish", + "German", + "Italian", + "French", + "Arabic", + "Africans", + "Hindi", + "Vietnamese", + "Urdu", + "Dutch", + "Quechua", + "Japanese", + "Chinese", + "Nepalese", + "Thai", + "Malay" + ], + "emp_id": "10508560", + "email": "deirdre@mcdiabetes.com" +} \ No newline at end of file diff --git a/test/tests/employee/mapping.json b/test/tests/employee/mapping.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/tests/employee/mapping.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/tests/employee/searches.json b/test/tests/employee/searches.json new file mode 100644 index 0000000..d4db280 --- /dev/null +++ b/test/tests/employee/searches.json @@ -0,0 +1,41 @@ +[ + { + "comment": "test array position output", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "manages.reports", + "term": "julián" + }, + "includeLocations": true + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "emp10508560", + "locations": { + "manages.reports": { + "julián": [ + { + "pos": 2, + "start": 7, + "end": 14, + "array_positions":[0] + }, + { + "pos": 2, + "start": 8, + "end": 15, + "array_positions":[3] + } + ] + } + } + } + ] + } + } +] \ No newline at end of file diff --git a/test/tests/facet/data/a.json b/test/tests/facet/data/a.json new file mode 100644 index 0000000..777b21f --- /dev/null +++ b/test/tests/facet/data/a.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "book", + "rating": 2, + "updated": "2014-11-25" +} \ No newline at end of file diff --git a/test/tests/facet/data/b.json b/test/tests/facet/data/b.json new file mode 100644 index 0000000..c3c2c69 --- /dev/null +++ b/test/tests/facet/data/b.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "book", + "rating": 7, + "updated": "2013-07-25" +} \ No newline at end of file diff --git a/test/tests/facet/data/c.json b/test/tests/facet/data/c.json new file mode 100644 index 0000000..da3542c --- /dev/null +++ b/test/tests/facet/data/c.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "book", + "rating": 1, + "updated": "2014-03-03" +} \ No newline at end of file diff --git a/test/tests/facet/data/d.json b/test/tests/facet/data/d.json new file mode 100644 index 0000000..20aaef1 --- /dev/null +++ b/test/tests/facet/data/d.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "book", + "rating": 9, + "updated": "2014-09-16" +} \ No newline at end of file diff --git a/test/tests/facet/data/e.json b/test/tests/facet/data/e.json new file mode 100644 index 0000000..8dbd28a --- /dev/null +++ b/test/tests/facet/data/e.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "book", + "rating": 5, + "updated": "2014-11-15" +} \ No newline at end of file diff --git a/test/tests/facet/data/f.json b/test/tests/facet/data/f.json new file mode 100644 index 0000000..74e8cd2 --- /dev/null +++ b/test/tests/facet/data/f.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "movie", + "rating": 3, + "updated": "2017-06-05" +} \ No newline at end of file diff --git a/test/tests/facet/data/g.json b/test/tests/facet/data/g.json new file mode 100644 index 0000000..ea5f29f --- /dev/null +++ b/test/tests/facet/data/g.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "movie", + "rating": 9, + "updated": "2011-10-03" +} \ No newline at end of file diff --git a/test/tests/facet/data/h.json b/test/tests/facet/data/h.json new file mode 100644 index 0000000..f91c0d7 --- /dev/null +++ b/test/tests/facet/data/h.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "movie", + "rating": 9, + "updated": "2019-08-26" +} \ No newline at end of file diff --git a/test/tests/facet/data/i.json b/test/tests/facet/data/i.json new file mode 100644 index 0000000..e46e5b7 --- /dev/null +++ b/test/tests/facet/data/i.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "movie", + "rating": 1, + "updated": "2014-12-14" +} \ No newline at end of file diff --git a/test/tests/facet/data/j.json b/test/tests/facet/data/j.json new file mode 100644 index 0000000..263f07b --- /dev/null +++ b/test/tests/facet/data/j.json @@ -0,0 +1,6 @@ +{ + "category": "inventory", + "type": "game", + "rating": 9, + "updated": "2013-10-20" +} \ No newline at end of file diff --git a/test/tests/facet/mapping.json b/test/tests/facet/mapping.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/tests/facet/mapping.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/tests/facet/searches.json b/test/tests/facet/searches.json new file mode 100644 index 0000000..6752282 --- /dev/null +++ b/test/tests/facet/searches.json @@ -0,0 +1,144 @@ +[ + { + "search": { + "from": 0, + "size": 0, + "query": { + "field": "category", + "term": "inventory" + }, + "facets": { + "types": { + "size": 3, + "field": "type" + } + } + }, + "result": { + "total_hits": 10, + "hits": [], + "facets": { + "types": { + "field": "type", + "total": 10, + "missing": 0, + "other": 0, + "terms": [ + { + "term": "book", + "count": 5 + }, + { + "term": "movie", + "count": 4 + }, + { + "term": "game", + "count": 1 + } + ] + } + } + } + }, + { + "search": { + "from": 0, + "size": 0, + "query": { + "field": "category", + "term": "inventory" + }, + "facets": { + "types": { + "size": 3, + "field": "rating", + "numeric_ranges": [ + { + "name": "low", + "max": 5 + }, + { + "name": "high", + "min": 5 + } + ] + } + } + }, + "result": { + "total_hits": 10, + "hits": [], + "facets": { + "types": { + "field": "rating", + "total": 10, + "missing": 0, + "other": 0, + "numeric_ranges": [ + { + "name": "high", + "count": 6, + "min": 5 + }, + { + "name": "low", + "count": 4, + "max": 5 + } + ] + } + } + } + }, + { + "search": { + "from": 0, + "size": 0, + "query": { + "field": "category", + "term": "inventory" + }, + "facets": { + "types": { + "size": 3, + "field": "updated", + "date_ranges": [ + { + "name": "old", + "end": "2012-01-01" + }, + { + "name": "new", + "start": "2012-01-01" + } + ] + } + } + }, + "result": { + "total_hits": 10, + "hits": [], + "facets": { + "types": { + "field": "updated", + "total": 10, + "missing": 0, + "other": 0, + "date_ranges": [ + { + "name": "new", + "count": 9, + "start": "2012-01-01T00:00:00Z" + }, + { + "name": "old", + "count": 1, + "end": "2012-01-01T00:00:00Z" + } + ] + } + } + } + } +] \ No newline at end of file diff --git a/test/tests/fosdem/data/3311@FOSDEM15@fosdem.org.json b/test/tests/fosdem/data/3311@FOSDEM15@fosdem.org.json new file mode 100644 index 0000000..62918db --- /dev/null +++ b/test/tests/fosdem/data/3311@FOSDEM15@fosdem.org.json @@ -0,0 +1,4 @@ +{ + "description": "From Prolog to Erlang to Haskell to Lisp to TLC and then back to Prolog I have journeyed, and I'd like to share some of the beautiful", + "category": "Word" +} \ No newline at end of file diff --git a/test/tests/fosdem/data/3492@FOSDEM15@fosdem.org.json b/test/tests/fosdem/data/3492@FOSDEM15@fosdem.org.json new file mode 100644 index 0000000..874f60e --- /dev/null +++ b/test/tests/fosdem/data/3492@FOSDEM15@fosdem.org.json @@ -0,0 +1,4 @@ +{ + "description": "different cats", + "category": "Perl" +} \ No newline at end of file diff --git a/test/tests/fosdem/data/3496@FOSDEM15@fosdem.org.json b/test/tests/fosdem/data/3496@FOSDEM15@fosdem.org.json new file mode 100644 index 0000000..1acdf34 --- /dev/null +++ b/test/tests/fosdem/data/3496@FOSDEM15@fosdem.org.json @@ -0,0 +1,4 @@ +{ + "description": "many cats", + "category": "Perl" +} \ No newline at end of file diff --git a/test/tests/fosdem/data/3505@FOSDEM15@fosdem.org.json b/test/tests/fosdem/data/3505@FOSDEM15@fosdem.org.json new file mode 100644 index 0000000..fb670aa --- /dev/null +++ b/test/tests/fosdem/data/3505@FOSDEM15@fosdem.org.json @@ -0,0 +1,4 @@ +{ + "description": "From Prolog to Erlang to Haskell to Lisp to TLC and then back to Prolog I have journeyed, and I'd like to share some of the beautiful", + "category": "Perl" +} \ No newline at end of file diff --git a/test/tests/fosdem/data/3507@FOSDEM15@fosdem.org.json b/test/tests/fosdem/data/3507@FOSDEM15@fosdem.org.json new file mode 100644 index 0000000..38be8e3 --- /dev/null +++ b/test/tests/fosdem/data/3507@FOSDEM15@fosdem.org.json @@ -0,0 +1,4 @@ +{ + "description": "From Prolog to Erlang to Haskell to Gel to TLC and then back to Prolog I have journeyed, and I'd like to share some of the beautiful", + "category": "Perl" +} \ No newline at end of file diff --git a/test/tests/fosdem/mapping.json b/test/tests/fosdem/mapping.json new file mode 100644 index 0000000..470b994 --- /dev/null +++ b/test/tests/fosdem/mapping.json @@ -0,0 +1,76 @@ +{ + "default_mapping": { + "enabled": true, + "dynamic": true, + "properties": { + "category": { + "enabled": true, + "dynamic": true, + "fields": [ + { + "type": "text", + "analyzer": "keyword", + "store": true, + "index": true, + "include_term_vectors": true, + "include_in_all": true + } + ], + "default_analyzer": "" + }, + "description": { + "enabled": true, + "dynamic": true, + "fields": [ + { + "type": "text", + "analyzer": "en", + "store": true, + "index": true, + "include_term_vectors": true, + "include_in_all": true + } + ], + "default_analyzer": "" + }, + "summary": { + "enabled": true, + "dynamic": true, + "fields": [ + { + "type": "text", + "analyzer": "en", + "store": true, + "index": true, + "include_term_vectors": true, + "include_in_all": true + } + ], + "default_analyzer": "" + }, + "url": { + "enabled": true, + "dynamic": true, + "fields": [ + { + "type": "text", + "analyzer": "keyword", + "store": true, + "index": true, + "include_term_vectors": true, + "include_in_all": true + } + ], + "default_analyzer": "" + } + }, + "default_analyzer": "" + }, + "type_field": "_type", + "default_type": "_default", + "default_analyzer": "en", + "default_datetime_parser": "dateTimeOptional", + "default_field": "_all", + "byte_array_converter": "json", + "analysis": {} +} \ No newline at end of file diff --git a/test/tests/fosdem/searches.json b/test/tests/fosdem/searches.json new file mode 100644 index 0000000..4909fea --- /dev/null +++ b/test/tests/fosdem/searches.json @@ -0,0 +1,109 @@ +[ + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "category", + "match_phrase": "Perl" + } + }, + "result": { + "total_hits": 4, + "hits": [ + { + "id": "3492@FOSDEM15@fosdem.org" + }, + { + "id": "3496@FOSDEM15@fosdem.org" + }, + { + "id": "3505@FOSDEM15@fosdem.org" + }, + { + "id": "3507@FOSDEM15@fosdem.org" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "match": "lisp" + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "3311@FOSDEM15@fosdem.org" + }, + { + "id": "3505@FOSDEM15@fosdem.org" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": {"boost":1,"query":"+lisp +category:Perl"} + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "3505@FOSDEM15@fosdem.org" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": {"boost":1,"query":"+lisp +category:\"Perl\""} + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "3505@FOSDEM15@fosdem.org" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "must": { + "conjuncts":[ + {"boost":1,"query":"+cats"}, + {"field":"category","match_phrase":"Perl"} + ] + } + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "3492@FOSDEM15@fosdem.org" + }, + { + "id": "3496@FOSDEM15@fosdem.org" + } + ] + } + } +] diff --git a/test/tests/geo/data/amoeba_brewery.json b/test/tests/geo/data/amoeba_brewery.json new file mode 100644 index 0000000..8997a24 --- /dev/null +++ b/test/tests/geo/data/amoeba_brewery.json @@ -0,0 +1 @@ +{"name":"amoeba brewery","city":"bangalore","state":"KAR","code":"","country":"India","phone":"","website":"","type":"brewery","updated":"2019-09-17 20:00:20","description":"brewery near cb office","address":[],"geo":{"accuracy":"APPROXIMATE","lat":12.97467,"lon":77.60490}} \ No newline at end of file diff --git a/test/tests/geo/data/brewpub_on_the_green.json b/test/tests/geo/data/brewpub_on_the_green.json new file mode 100644 index 0000000..03f50f4 --- /dev/null +++ b/test/tests/geo/data/brewpub_on_the_green.json @@ -0,0 +1 @@ +{"name":"Brewpub-on-the-Green","city":"Fremont","state":"California","code":"","country":"United States","phone":"","website":"","type":"brewery","updated":"2010-07-22 20:00:20","description":"","address":[],"geo":{"accuracy":"APPROXIMATE","lat":37.5483,"lon":-121.989}} \ No newline at end of file diff --git a/test/tests/geo/data/capital_city_brewing_company.json b/test/tests/geo/data/capital_city_brewing_company.json new file mode 100644 index 0000000..0b2f2f7 --- /dev/null +++ b/test/tests/geo/data/capital_city_brewing_company.json @@ -0,0 +1 @@ +{"name":"Capital City Brewing Company","city":"Washington","state":"District of Columbia","code":"20005","country":"United States","phone":"202.628.2222","website":"http://www.capcitybrew.com","type":"brewery","updated":"2010-07-22 20:00:20","description":"Washington DC's first brewpub since prohibition, Capitol City Brewing Co. opened its doors in 1992. Our first location still stands in Downtown DC, at 11th and H St., NW. Our company policy is to bring the fine craft of brewing to every person who lives and visits our region, as well as treating them to a wonderful meal and a great experience.","address":["1100 New York Ave, NW"],"geo":{"accuracy":"ROOFTOP","lat":38.8999,"lon":-77.0272}} \ No newline at end of file diff --git a/test/tests/geo/data/communiti_brewery.json b/test/tests/geo/data/communiti_brewery.json new file mode 100644 index 0000000..832ae91 --- /dev/null +++ b/test/tests/geo/data/communiti_brewery.json @@ -0,0 +1 @@ +{"name":"communiti brewery","city":"bangalore","state":"KAR","code":"","country":"India","phone":"","website":"","type":"brewery","updated":"2019-09-17 20:00:20","description":"brewery near cb office","address":[],"geo":{"accuracy":"APPROXIMATE","lat":12.97237,"lon":77.608237}} \ No newline at end of file diff --git a/test/tests/geo/data/firehouse_grill_brewery.json b/test/tests/geo/data/firehouse_grill_brewery.json new file mode 100644 index 0000000..c2e2864 --- /dev/null +++ b/test/tests/geo/data/firehouse_grill_brewery.json @@ -0,0 +1 @@ +{"name":"Firehouse Grill & Brewery","city":"Sunnyvale","state":"California","code":"94086","country":"United States","phone":"1-408-773-9500","website":"","type":"brewery","updated":"2010-07-22 20:00:20","description":"","address":["111 South Murphy Avenue"],"geo":{"accuracy":"RANGE_INTERPOLATED","lat":37.3775,"lon":-122.03}} \ No newline at end of file diff --git a/test/tests/geo/data/hook_ladder_brewing_company.json b/test/tests/geo/data/hook_ladder_brewing_company.json new file mode 100644 index 0000000..b0f0398 --- /dev/null +++ b/test/tests/geo/data/hook_ladder_brewing_company.json @@ -0,0 +1 @@ +{"name":"Hook & Ladder Brewing Company","city":"Silver Spring","state":"Maryland","code":"20910","country":"United States","phone":"301.565.4522","website":"http://www.hookandladderbeer.com","type":"brewery","updated":"2010-07-22 20:00:20","description":"At Hook & Ladder Brewing we believe in great beer in the company of good friends, so we bring you three great beers for your drinking pleasure (please drink responsibly). Each of our beers is carefully crafted with the finest quality ingredients for a distinctive taste we know you will enjoy. Try one tonight, you just might get hooked. Through our own experiences in the fire and rescue service we have chosen the Hook & Ladder as a symbol of pride and honor to pay tribute to the brave men and women who serve and protect our communities.","address":["8113 Fenton St."],"geo":{"accuracy":"ROOFTOP","lat":38.9911,"lon":-77.0237}} \ No newline at end of file diff --git a/test/tests/geo/data/jack_s_brewing.json b/test/tests/geo/data/jack_s_brewing.json new file mode 100644 index 0000000..4298cbd --- /dev/null +++ b/test/tests/geo/data/jack_s_brewing.json @@ -0,0 +1 @@ +{"name":"Jack's Brewing","city":"Fremont","state":"California","code":"94538","country":"United States","phone":"1-510-796-2036","website":"","type":"brewery","updated":"2010-07-22 20:00:20","description":"","address":["39176 Argonaut Way"],"geo":{"accuracy":"ROOFTOP","lat":37.5441,"lon":-121.988}} \ No newline at end of file diff --git a/test/tests/geo/data/social_brewery.json b/test/tests/geo/data/social_brewery.json new file mode 100644 index 0000000..ae636ad --- /dev/null +++ b/test/tests/geo/data/social_brewery.json @@ -0,0 +1 @@ +{"name":"social brewery","city":"bangalore","state":"KAR","code":"","country":"India","phone":"","website":"","type":"brewery","updated":"2019-09-17 20:00:20","description":"brewery near cb office, but outside the polygon","address":[],"geo":{"accuracy":"APPROXIMATE","lat":12.9736946,"lon":77.6042133}} \ No newline at end of file diff --git a/test/tests/geo/data/sweet_water_tavern_and_brewery.json b/test/tests/geo/data/sweet_water_tavern_and_brewery.json new file mode 100644 index 0000000..6a675b5 --- /dev/null +++ b/test/tests/geo/data/sweet_water_tavern_and_brewery.json @@ -0,0 +1 @@ +{"name":"Sweet Water Tavern and Brewery","city":"Sterling","state":"Virginia","code":"20121","country":"United States","phone":"(703) 449-1108","website":"http://www.greatamericanrestaurants.com/sweetMainSter/index.htm","type":"brewery","updated":"2010-07-22 20:00:20","description":"","address":["45980 Waterview Plaza"],"geo":{"accuracy":"RANGE_INTERPOLATED","lat":39.0324,"lon":-77.4097}} \ No newline at end of file diff --git a/test/tests/geo/mapping.json b/test/tests/geo/mapping.json new file mode 100644 index 0000000..f067367 --- /dev/null +++ b/test/tests/geo/mapping.json @@ -0,0 +1,36 @@ +{ + "types": { + "brewery": { + "properties": { + "name": { + "fields": [ + { + "include_term_vectors": true, + "include_in_all": true, + "index": true, + "store": true, + "analyzer": "keyword", + "type": "text" + } + ], + "dynamic": true, + "enabled": true + }, + "geo": { + "fields": [ + { + "include_term_vectors": true, + "include_in_all": true, + "index": true, + "store": true, + "type": "geopoint" + } + ], + "dynamic": true, + "enabled": true + } + } + } + }, + "default_type": "brewery" +} diff --git a/test/tests/geo/searches.json b/test/tests/geo/searches.json new file mode 100644 index 0000000..20646b4 --- /dev/null +++ b/test/tests/geo/searches.json @@ -0,0 +1,326 @@ +[ + { + "comment": "breweries near the couchbase office", + "search": { + "from": 0, + "size": 10, + "query": { + "location": { + "lon": -122.107799, + "lat": 37.399285 + }, + "distance": "100mi", + "field": "geo" + }, + "sort": [ + { + "by": "geo_distance", + "field": "geo", + "unit": "mi", + "location": { + "lon": -122.107799, + "lat": 37.399285 + } + } + ] + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "firehouse_grill_brewery" + }, + { + "id": "jack_s_brewing" + }, + { + "id": "brewpub_on_the_green" + } + ] + } + }, + { + "comment": "breweries near the whitehouse", + "search": { + "from": 0, + "size": 10, + "query": { + "location": { + "lon": -77.0365, + "lat": 38.8977 + }, + "distance": "100mi", + "field": "geo" + }, + "sort": [ + { + "by": "geo_distance", + "field": "geo", + "unit": "mi", + "location": { + "lon": -77.0365, + "lat": 38.8977 + } + } + ] + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "capital_city_brewing_company" + }, + { + "id": "hook_ladder_brewing_company" + }, + { + "id": "sweet_water_tavern_and_brewery" + } + ] + } + }, + { + "comment": "bounding box of USA", + "search": { + "from": 0, + "size": 10, + "query": { + "top_left": { + "lon": -125.0011, + "lat": 49.5904 + }, + "bottom_right": { + "lon": -66.9326, + "lat": 24.9493 + }, + "field": "geo" + }, + "sort": [ + "name" + ] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "brewpub_on_the_green" + }, + { + "id": "capital_city_brewing_company" + }, + { + "id": "firehouse_grill_brewery" + }, + { + "id": "hook_ladder_brewing_company" + }, + { + "id": "jack_s_brewing" + }, + { + "id": "sweet_water_tavern_and_brewery" + } + ] + } + }, + { + "comment": "bounding box around DC area", + "search": { + "from": 0, + "size": 10, + "query": { + "top_left": { + "lon": -78, + "lat": 39.5 + }, + "bottom_right": { + "lon": -76, + "lat": 38.5 + }, + "field": "geo" + }, + "sort": [ + "name" + ] + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "capital_city_brewing_company" + }, + { + "id": "hook_ladder_brewing_company" + }, + { + "id": "sweet_water_tavern_and_brewery" + } + ] + } + }, + { + "comment": "breweries near the couchbase office, using GeoJSON style points", + "search": { + "from": 0, + "size": 10, + "query": { + "location": [-122.107799,37.399285], + "distance": "100mi", + "field": "geo" + }, + "sort": [ + { + "by": "geo_distance", + "field": "geo", + "unit": "mi", + "location": [-122.107799,37.399285] + } + ] + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "firehouse_grill_brewery" + }, + { + "id": "jack_s_brewing" + }, + { + "id": "brewpub_on_the_green" + } + ] + } + }, + { + "comment": "bounding box around DC area, using GeoJSON style", + "search": { + "from": 0, + "size": 10, + "query": { + "top_left": [-78,39.5], + "bottom_right": [-76,38.5], + "field": "geo" + }, + "sort": [ + "name" + ] + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "capital_city_brewing_company" + }, + { + "id": "hook_ladder_brewing_company" + }, + { + "id": "sweet_water_tavern_and_brewery" + } + ] + } + }, + { + "comment": "polygon around cb office area, using GeoJSON lat/lon as array", + "search": { + "from": 0, + "size": 10, + "query": { + "polygon_points": [[77.607749,12.974872],[77.6101101,12.971725],[77.606912,12.972530],[77.603780,12.975112]], + "field": "geo" + }, + "sort": [ + "name" + ] + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "amoeba_brewery" + }, + { + "id": "communiti_brewery" + } + ] + } + }, + { + "comment": "polygon around cb office area, using GeoJSON lat/lon as string", + "search": { + "from": 0, + "size": 10, + "query": { + "polygon_points": ["12.974872, 77.607749","12.971725, 77.6101101","12.972530, 77.606912","12.975112, 77.603780"], + "field": "geo" + }, + "sort": [ + "name" + ] + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "amoeba_brewery" + }, + { + "id": "communiti_brewery" + } + ] + } + }, + { + "comment": "polygon around cb office area", + "search": { + "from": 0, + "size": 10, + "query": { + "polygon_points": [{"lat":12.974872, "lon":77.607749}, {"lat":12.971725, "lon":77.6101101}, + {"lat":12.972530, "lon":77.606912}, {"lat":12.975112, "lon":77.603780}], + "field": "geo" + }, + "sort": [ + "name" + ] + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "amoeba_brewery" + }, + { + "id": "communiti_brewery" + } + ] + } + }, + { + "comment": "polygon around cb office area as geohash", + "search": { + "from": 0, + "size": 10, + "query": { + "polygon_points": ["tdr1y40", "tdr1y13", "tdr1vcx", "tdr1vfj"], + "field": "geo" + }, + "sort": [ + "name" + ] + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "amoeba_brewery" + }, + { + "id": "communiti_brewery" + } + ] + } + } +] + diff --git a/test/tests/geoshapes/data/circle_halairport.json b/test/tests/geoshapes/data/circle_halairport.json new file mode 100644 index 0000000..d7e6a13 --- /dev/null +++ b/test/tests/geoshapes/data/circle_halairport.json @@ -0,0 +1,14 @@ +{ + "name": "hal airpork circular region", + "city": "bangalore", + "type": "geoshapes", + "description": "circle covering the hal airport", + "region": { + "type": "Circle", + "coordinates": [ + 77.6698637008667, + 12.951865687866821 + ], + "radius": "2.4km" + } +} \ No newline at end of file diff --git a/test/tests/geoshapes/data/envelope_brockwell_park.json b/test/tests/geoshapes/data/envelope_brockwell_park.json new file mode 100644 index 0000000..c868f04 --- /dev/null +++ b/test/tests/geoshapes/data/envelope_brockwell_park.json @@ -0,0 +1,19 @@ +{ + "name": "brockwell park envelope", + "city": "london", + "type": "geoshapes", + "description": "brockwell park envelope", + "region": { + "type": "envelope", + "coordinates": [ + [ + -0.11278152465820314, + 51.44579626059569 + ], + [ + -0.10037899017333984, + 51.45566490761856 + ] + ] + } +} \ No newline at end of file diff --git a/test/tests/geoshapes/data/geometrycollection_tvm.json b/test/tests/geoshapes/data/geometrycollection_tvm.json new file mode 100644 index 0000000..5b08c90 --- /dev/null +++ b/test/tests/geoshapes/data/geometrycollection_tvm.json @@ -0,0 +1,165 @@ +{ + "name": "geometrycollection comprised of various shapes", + "city": "bangalore", + "type": "geoshapes", + "description": "geometrycollection comprised of various shapes", + "region": { + "type": "geometrycollection", + "geometries": [ + { + "type": "point", + "coordinates": [ + 76.92815780639648, + 8.525851789596233 + ] + }, + { + "type": "LineString", + "coordinates": [ + [ + 76.92867279052734, + 8.490369393806219 + ], + [ + 76.94377899169922, + 8.494104537551882 + ] + ] + }, + { + "type": "polygon", + "coordinates": [ + [ + [ + 76.92815780639648, + 8.525851789596233 + ], + [ + 76.92060470581055, + 8.520504174874656 + ], + [ + 76.92206382751465, + 8.519061154914393 + ], + [ + 76.92824363708496, + 8.519061154914393 + ], + [ + 76.92970275878906, + 8.523475081176768 + ], + [ + 76.92815780639648, + 8.525851789596233 + ] + ] + ] + }, + { + "type": "multipoint", + "coordinates": [ + [ + 76.90670013427733, + 8.497839644932787 + ], + [ + 76.94137573242188, + 8.485275957394883 + ] + ] + }, + { + "type": "multiLineString", + "coordinates": [ + [ + [ + 76.89322471618651, + 8.521522773921424 + ], + [ + 76.89648628234863, + 8.518042549311815 + ] + ], + [ + [ + 76.9068717956543, + 8.494783650690053 + ], + [ + 76.93296432495117, + 8.468552033040881 + ] + ] + ] + }, + { + "type": "multipolygon", + "coordinates": [ + [ + [ + [ + 76.90249443054199, + 8.546138091708775 + ], + [ + 76.89983367919922, + 8.541300033890494 + ], + [ + 76.90498352050781, + 8.53985709248573 + ], + [ + 76.90858840942383, + 8.54520443620746 + ], + [ + 76.90712928771973, + 8.548090273095957 + ], + [ + 76.90249443054199, + 8.546138091708775 + ] + ] + ], + [ + [ + [ + 76.88326835632324, + 8.564131732621458 + ], + [ + 76.88429832458496, + 8.555729147617923 + ], + [ + 76.88893318176268, + 8.552079482230221 + ], + [ + 76.89339637756348, + 8.55369212938781 + ], + [ + 76.89494132995605, + 8.56133089156368 + ], + [ + 76.89116477966309, + 8.566423314514562 + ], + [ + 76.88326835632324, + 8.564131732621458 + ] + ] + ] + ] + } + ] + } +} diff --git a/test/tests/geoshapes/data/linestring_putney_bridge.json b/test/tests/geoshapes/data/linestring_putney_bridge.json new file mode 100644 index 0000000..cf82a36 --- /dev/null +++ b/test/tests/geoshapes/data/linestring_putney_bridge.json @@ -0,0 +1,19 @@ +{ + "name": "linestring for putney bridge", + "city": "london", + "type": "geoshapes", + "description": "linestring for putney bridge", + "region": { + "type": "linestring", + "coordinates": [ + [ + -0.21183013916015625, + 51.46791083061189 + ], + [ + -0.21431922912597656, + 51.465504685939706 + ] + ] + } +} \ No newline at end of file diff --git a/test/tests/geoshapes/data/multilinestring_old_airport_road.json b/test/tests/geoshapes/data/multilinestring_old_airport_road.json new file mode 100644 index 0000000..8799fa2 --- /dev/null +++ b/test/tests/geoshapes/data/multilinestring_old_airport_road.json @@ -0,0 +1,48 @@ +{ + "name": "road routes", + "city": "bangalore", + "type": "geoshapes", + "description": "multilinestrings approximating the roads indiranagar 100ft and old airport port road", + "region": { + "type": "multilinestring", + "coordinates": [ + [ + [ + 77.64081001281738, + 12.983398626256326 + ], + [ + 77.64166831970213, + 12.960648472679763 + ] + ], + [ [ + 77.64192581176758, + 12.960564828571133 + ], + [ + 77.66990661621094, + 12.958390071883693 + ] + ], + [ [ + 77.67016410827637, + 12.958055492245812 + ], + [ + 77.68106460571289, + 12.954626025039444 + ] + ], + [ [ + 77.68149375915527, + 12.954542378907867 + ], + [ + 77.7011489868164, + 12.957219041184294 + ] + ] + ] + } +} diff --git a/test/tests/geoshapes/data/multipoint_blr_stadiums.json b/test/tests/geoshapes/data/multipoint_blr_stadiums.json new file mode 100644 index 0000000..b21fb1d --- /dev/null +++ b/test/tests/geoshapes/data/multipoint_blr_stadiums.json @@ -0,0 +1,23 @@ +{ + "name": "multipoints for stadiums", + "city": "bangalore", + "type": "geoshapes", + "description": "contains 3 points", + "region": { + "type": "multipoint", + "coordinates": [ + [ + 77.5929594039917, + 12.969347306502671 + ], + [ + 77.6004695892334, + 12.979007674139009 + ], + [ + 77.60068416595459, + 12.961735843534306 + ] + ] + } +} \ No newline at end of file diff --git a/test/tests/geoshapes/data/multipolygon_london_parks.json b/test/tests/geoshapes/data/multipolygon_london_parks.json new file mode 100644 index 0000000..659773a --- /dev/null +++ b/test/tests/geoshapes/data/multipolygon_london_parks.json @@ -0,0 +1,87 @@ +{ + "name": "london parks as multipolygon", + "city": "london", + "type": "geoshapes", + "description": "multipolygon with london", + "region": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + -0.163421630859375, + 51.531600743186644 + ], + [ + -0.15277862548828125, + 51.52455221546295 + ], + [ + -0.14556884765625, + 51.524979430024345 + ], + [ + -0.14591217041015625, + 51.536085601784755 + ], + [ + -0.15895843505859375, + 51.53693981046689 + ], + [ + -0.163421630859375, + 51.531600743186644 + ] + ] + ], + [ + [ + [ + -0.1902008056640625, + 51.5091698216777 + ], + [ + -0.1888275146484375, + 51.50147667659363 + ], + [ + -0.15071868896484375, + 51.503186376638006 + ], + [ + -0.1599884033203125, + 51.51322956905176 + ], + [ + -0.1902008056640625, + 51.5091698216777 + ] + ] + ], + [ + [ + [ + -0.16582489013671875, + 51.4811690848672 + ], + [ + -0.1635932922363281, + 51.474861202507434 + ], + [ + -0.14883041381835938, + 51.47764105478667 + ], + [ + -0.14951705932617188, + 51.48352095330697 + ], + [ + -0.16582489013671875, + 51.4811690848672 + ] + ] + ] + ] + } +} \ No newline at end of file diff --git a/test/tests/geoshapes/data/point_museum_of_london.json b/test/tests/geoshapes/data/point_museum_of_london.json new file mode 100644 index 0000000..1a74a2e --- /dev/null +++ b/test/tests/geoshapes/data/point_museum_of_london.json @@ -0,0 +1,13 @@ +{ + "name": "geopoint for the museum of london", + "city": "london", + "type": "geoshapes", + "description": "geopoint for the museum of london", + "region": { + "type": "point", + "coordinates": [ + -0.09613037109375, + 51.51803669675129 + ] + } +} \ No newline at end of file diff --git a/test/tests/geoshapes/data/polygon_cubbonpark.json b/test/tests/geoshapes/data/polygon_cubbonpark.json new file mode 100644 index 0000000..c4865a0 --- /dev/null +++ b/test/tests/geoshapes/data/polygon_cubbonpark.json @@ -0,0 +1,45 @@ +{ + "name": "cubbon park polygon", + "city": "bangalore", + "type": "geoshapes", + "description": "polygon inside cubbon park", + "region": { + "type": "Polygon", + "coordinates": [ + [ + [ + 77.58894681930542, + 12.976498523818783 + ], + [ + 77.58677959442139, + 12.974533005048169 + ], + [ + 77.5879168510437, + 12.971333776381767 + ], + [ + 77.58849620819092, + 12.96800904416803 + ], + [ + 77.59371042251587, + 12.972128359891645 + ], + [ + 77.59512662887573, + 12.973842978816679 + ], + [ + 77.59253025054932, + 12.976853988320428 + ], + [ + 77.58894681930542, + 12.976498523818783 + ] + ] + ] + } +} \ No newline at end of file diff --git a/test/tests/geoshapes/mapping.json b/test/tests/geoshapes/mapping.json new file mode 100644 index 0000000..3e74202 --- /dev/null +++ b/test/tests/geoshapes/mapping.json @@ -0,0 +1,36 @@ +{ + "types": { + "geoshapes": { + "properties": { + "name": { + "fields": [ + { + "include_term_vectors": true, + "include_in_all": true, + "index": true, + "store": true, + "analyzer": "keyword", + "type": "text" + } + ], + "dynamic": true, + "enabled": true + }, + "region": { + "fields": [ + { + "include_term_vectors": true, + "include_in_all": true, + "index": true, + "store": true, + "type": "geoshape" + } + ], + "dynamic": true, + "enabled": true + } + } + } + }, + "default_type": "geoshapes" +} \ No newline at end of file diff --git a/test/tests/geoshapes/searches.json b/test/tests/geoshapes/searches.json new file mode 100644 index 0000000..6027a18 --- /dev/null +++ b/test/tests/geoshapes/searches.json @@ -0,0 +1,1500 @@ +[ + { + "comment": "search with a circular shape within cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "Circle", + "coordinates": [ + 77.59092092514038, + 12.975494856600474 + ], + "radius": "0.1km" + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with a circular shape within cubbon park polygon, (circle doesn't fully contained within)", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + 77.59092092514038, + 12.975494856600474 + ], + "radius": "150m" + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "search with a polygon that contains the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "Polygon", + "coordinates": [ + [ + [ + 77.58617877960205, + 12.9772303619447 + ], + [ + 77.58630752563477, + 12.966419848296587 + ], + [ + 77.59802341461182, + 12.968887279637073 + ], + [ + 77.5989246368408, + 12.980304058548604 + ], + [ + 77.58617877960205, + 12.9772303619447 + ] + ] + ] + }, + "relation": "within" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with a multipolygon that intersects the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "multipolygon", + "coordinates": [ + [ + [ + [ + 77.58268117904663, + 12.980513152175025 + ], + [ + 77.58147954940794, + 12.977983107483992 + ], + [ + 77.58708000183104, + 12.97886130773254 + ], + [ + 77.58268117904663, + 12.980513152175025 + ] + ] + ], + [ + [ + [ + 77.5864577293396, + 12.97762764459667 + ], + [ + 77.58879661560059, + 12.975076660730531 + ], + [ + 77.59115695953369, + 12.979216768855913 + ], + [ + 77.5864577293396, + 12.97762764459667 + ] + ] + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with multilinestrings that intersects the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "multilinestring", + "coordinates": [ + [ + [ + 77.58761644363403, + 12.974302996517075 + ], + [ + 77.59319543838501, + 12.978401298465434 + ] + ], + [ + [ + 77.5947618484497, + 12.98500862259466 + ], + [ + 77.59808778762817, + 12.983565899088745 + ] + ], + [ + [ + 77.60109186172485, + 12.973529329896703 + ], + [ + 77.59943962097168, + 12.970225537247586 + ] + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with multilinestrings that aren't contained within the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "multilinestring", + "coordinates": [ + [ + [ + 77.58761644363403, + 12.974302996517075 + ], + [ + 77.59319543838501, + 12.978401298465434 + ] + ], + [ + [ + 77.5947618484497, + 12.98500862259466 + ], + [ + 77.59808778762817, + 12.983565899088745 + ] + ], + [ + [ + 77.60109186172485, + 12.973529329896703 + ], + [ + 77.59943962097168, + 12.970225537247586 + ] + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "search with multilinestrings that are all contained within the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "multilinestring", + "coordinates": [ + [ + [ + 77.59107112884521, + 12.975243939162915 + ], + [ + 77.59190797805786, + 12.973842978816679 + ] + ], + [ + [ + 77.58954763412476, + 12.970685561638497 + ], + [ + 77.59117841720581, + 12.971835618893842 + ] + ], + [ + [ + 77.58851766586304, + 12.973152950670608 + ], + [ + 77.58937597274779, + 12.972212000113458 + ] + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with point that is contained within the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "Point", + "coordinates": [ + 77.59107112884521, + 12.975243939162915 + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with an envelope that is within the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "Envelope", + "coordinates": [ + [ + 77.59158611297607, + 12.9720028995062035 + ], + [ + 77.59263753890991, + 12.973173860642571 + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with an envelope that contains the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "Envelope", + "coordinates": [ + [ + 77.57969856262207, + 12.9641614998626 + ], + [ + 77.60295867919922, + 12.989336742847172 + ] + ] + }, + "relation": "within" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with a geometrycollection that is within the cubbon park polygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "geometrycollection", + "geometries": [ + { + "type": "point", + "coordinates": [ + 77.59158611297607, + 12.972002899506203 + ] + }, + { + "type": "LineString", + "coordinates": [ + [ + 77.58851766586304, + 12.973152950670608 + ], + [ + 77.58937597274779, + 12.972212000113458 + ] + ] + }, + { + "type": "polygon", + "coordinates": [ + [ + [ + 77.59055614471436, + 12.974721193688106 + ], + [ + 77.58954763412476, + 12.97350841995465 + ], + [ + 77.59141445159912, + 12.973382960265356 + ], + [ + 77.59055614471436, + 12.974721193688106 + ] + ] + ] + } + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with a polygon that intersects the hal airport region", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "polygon", + "coordinates": [ + [ + [ + 77.67934799194336, + 12.938147195017896 + ], + [ + 77.66793251037598, + 12.930492951786736 + ], + [ + 77.67711639404297, + 12.922127390141315 + ], + [ + 77.67934799194336, + 12.938147195017896 + ] + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "circle_halairport" + } + ] + } + }, + { + "comment": "search with a linestring that intersects the hal airport and cubbon park region", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "linestring", + "coordinates": [ + [ + 77.59042739868164, + 12.973529329896703 + ], + [ + 77.65892028808594, + 12.950109093741462 + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "circle_halairport" + }, + { + "id": "polygon_cubbonpark" + } + ] + } + }, + { + "comment": "search with an envelope within the circle_halairport", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "envelope", + "coordinates": [ + [ + 77.65625953674316, + 12.943249893344905 + ], + [ + 77.68355369567871, + 12.945843027882455 + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "circle_halairport" + } + ] + } + }, + { + "comment": "search with a circle which intersects the road multilinestring and the hal circle", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + 77.68132209777832, + 12.954918786278716 + ], + "radius": "50m" + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 2, + "hits": [ + { + "id": "circle_halairport" + }, + { + "id": "multilinestring_old_airport_road" + } + ] + } + }, + { + "comment": "search with a polygon which intersects the road multilinestring", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "polygon", + "coordinates": [ + [ + [ + 77.64102458953856, + 12.97751264178902 + ], + [ + 77.64109969139099, + 12.975317123441693 + ], + [ + 77.64338493347168, + 12.976728530319054 + ], + [ + 77.64102458953856, + 12.97751264178902 + ] + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multilinestring_old_airport_road" + } + ] + } + }, + { + "comment": "search with a linestring which intersects the road multilinestring", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "linestring", + "coordinates": [ + [ + 77.63969421386717, + 12.978265386473618 + ], + [ + 77.64354586601257, + 12.978453572288663 + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multilinestring_old_airport_road" + } + ] + } + }, + { + "comment": "search with an envelope which intersects the road multilinestring", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "envelope", + "coordinates": [ + [ + 77.64100313186644, + 12.95902786307307 + ], + [ + 77.6419472694397, + 12.96069029472353 + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multilinestring_old_airport_road" + } + ] + } + }, + { + "comment": "search with multipoint which are contained within the multipolygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "multipoint", + "coordinates": [ + [ + -0.14797210693359375, + 51.52615424940099 + ], + [ + -0.16857147216796875, + 51.50863561745838 + ], + [ + -0.15535354614257812, + 51.48010001366223 + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multipolygon_london_parks" + } + ] + } + }, + { + "comment": "search with multilinestring that are contained within the multipolygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "multilinestring", + "coordinates": [ + [ + [ + -0.17063140869140625, + 51.50884929989774 + ], + [ + -0.15655517578125, + 51.5072466571743 + ] + ], + [ + [ + -0.16222000122070312, + 51.47988619641402 + ], + [ + -0.15466690063476562, + 51.48074145939243 + ] + ], + [ + [ + -0.15844345092773438, + 51.53245503603458 + ], + [ + -0.15123367309570312, + 51.53170753066937 + ] + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multipolygon_london_parks" + } + ] + } + }, + { + "comment": "search with multilinestring out of which one isn't contained within the multipolygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "multilinestring", + "coordinates": [ + [ + [ + -0.17063140869140625, + 51.50884929989774 + ], + [ + -0.15655517578125, + 51.5072466571743 + ] + ], + [ + [ + -0.16222000122070312, + 51.47988619641402 + ], + [ + -0.15466690063476562, + 51.48074145939243 + ] + ], + [ + [ + -0.15844345092773438, + 51.53245503603458 + ], + [ + -0.15123367309570312, + 51.53170753066937 + ] + ], + [ + [ + -0.08651733398437499, + 51.51013137348817 + ], + [ + -0.08909225463867188, + 51.50543026060529 + ] + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "search with a geometrycollection that contains the london_parks_multipolygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "geometrycollection", + "geometries": [ + { + "type": "multipolygon", + "coordinates": [ + [ + [ + [ + -0.19517898559570312, + 51.51344322994464 + ], + [ + -0.19277572631835938, + 51.49292721420451 + ], + [ + -0.14110565185546875, + 51.49773648412071 + ], + [ + -0.14471054077148438, + 51.51889124411907 + ], + [ + -0.19517898559570312, + 51.51344322994464 + ] + ] + ], + [ + [ + [ + -0.16925811767578122, + 51.48373475351443 + ], + [ + -0.16925811767578122, + 51.47004951935931 + ], + [ + -0.14608383178710938, + 51.472722739318336 + ], + [ + -0.14453887939453125, + 51.48758298584306 + ], + [ + -0.16925811767578122, + 51.48373475351443 + ] + ] + ] + ] + }, + { + "type": "LineString", + "coordinates": [ + [ + 77.58851766586304, + 12.973152950670608 + ], + [ + 77.58937597274779, + 12.972212000113458 + ] + ] + }, + { + "type": "polygon", + "coordinates": [ + [ + [ + -0.17337799072265625, + 51.54323910441573 + ], + [ + -0.1668548583984375, + 51.51889124411907 + ], + [ + -0.09286880493164062, + 51.53341609632549 + ], + [ + -0.17337799072265625, + 51.54323910441573 + ] + ] + ] + } + ] + }, + "relation": "within" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multipolygon_london_parks" + } + ] + } + }, + { + "comment": "search with a circle that intersects with one of the polygons in the multipolygon_london_parks", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + -0.14265060424804688, + 51.53298896092339 + ], + "radius": "550m" + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multipolygon_london_parks" + } + ] + } + }, + { + "comment": "search with a circle that contains london museum geopoint", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + -0.09115219116210938, + 51.516487788780005 + ], + "radius": "1050m" + }, + "relation": "within" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "point_museum_of_london" + } + ] + } + }, + { + "comment": "search with brockwell park polygon that is contained within brockwell park envelope", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "polygon", + "coordinates": [ + [ + [ + -0.11149406433105469, + 51.454942883825744 + ], + [ + -0.11230945587158205, + 51.45218839188088 + ], + [ + -0.11136531829833984, + 51.450530268053605 + ], + [ + -0.1117086410522461, + 51.44873835686053 + ], + [ + -0.11016368865966797, + 51.446010237625224 + ], + [ + -0.10497093200683594, + 51.446705656046376 + ], + [ + -0.10192394256591797, + 51.4490058107573 + ], + [ + -0.1007223129272461, + 51.45085119994589 + ], + [ + -0.10188102722167967, + 51.45218839188088 + ], + [ + -0.10681629180908203, + 51.45368600035086 + ], + [ + -0.10715961456298828, + 51.453338345620416 + ], + [ + -0.11149406433105469, + 51.454942883825744 + ] + ] + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "envelope_brockwell_park" + } + ] + } + }, + { + "comment": "search with point that is contained within brockwell park envelope", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "point", + "coordinates": [ + -0.10074377059936523, + 51.450824455707696 + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "envelope_brockwell_park" + } + ] + } + }, + { + "comment": "search with linestring that intersects the putney bridge", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "LineString", + "coordinates": [ + [ + -0.2171945571899414, + 51.46876631814087 + ], + [ + -0.2064228057861328, + 51.464943233925986 + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "linestring_putney_bridge" + } + ] + } + }, + { + "comment": "search with polygon that contains the blr stadiums/multipoint", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "polygon", + "coordinates": [ + [ + [ + 77.60107040405273, + 12.981349524921757 + ], + [ + 77.59270191192627, + 12.969180024104505 + ], + [ + 77.60089874267577, + 12.961024870820744 + ], + [ + 77.60107040405273, + 12.981349524921757 + ] + ] + ] + }, + "relation": "within" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "multipoint_blr_stadiums" + } + ] + } + }, + { + "comment": "search a point that is within the multipolygon of the geometrycollection", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "point", + "coordinates": [ + 76.88919067382812, + 8.556238400473156 + ] + }, + "relation": "contains" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "geometrycollection_tvm" + } + ] + } + }, + { + "comment": "search an envelope that intersects with the polygon of the geometrycollection", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "envelope", + "coordinates": [ + [ + 76.91880226135254, + 8.515665792358828 + ], + [ + 76.92523956298828, + 8.525427378462332 + ] + ] + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "geometrycollection_tvm" + } + ] + } + }, + { + "comment": "search a circle that intersects with the linestring of the geometrycollection", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + 76.91305160522461, + 8.477890354619287 + ], + "radius": "1mi" + }, + "relation": "intersects" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "geometrycollection_tvm" + } + ] + } + }, + { + "comment": "search a circle that contains the entire geometrycollection", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + 76.93622589111328, + 8.501574715933401 + ], + "radius": "10mi" + }, + "relation": "within" + } + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "geometrycollection_tvm" + } + ] + } + }, + { + "comment": "search a polygon that contains the entire geometrycollection, circle, multilinestring, polygon, multipoint", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + 77.71728515624999, + 12.060809058367294 + ], + "radius": "1000mi" + }, + "relation": "within" + } + }, + "sort": ["-_id"] + }, + "result": { + "total_hits": 5, + "hits": [ + { + "id": "polygon_cubbonpark" + }, + { + "id": "multipoint_blr_stadiums" + }, + { + "id": "multilinestring_old_airport_road" + }, + { + "id": "geometrycollection_tvm" + }, + { + "id": "circle_halairport" + } + ] + } + }, + { + "comment": "search circle that contains the envelope, linestring, point, multipolygon", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "circle", + "coordinates": [ + -0.23277282714843747, + 51.45828549061808 + ], + "radius": "1000mi" + }, + "relation": "within" + } + }, + "sort": ["-_id"] + }, + "result": { + "total_hits": 4, + "hits": [ + { + "id": "point_museum_of_london" + }, + { + "id": "multipolygon_london_parks" + }, + { + "id": "linestring_putney_bridge" + }, + { + "id": "envelope_brockwell_park" + } + ] + } + }, + { + "comment": "search a polygon(almost the whole earth surface) that contains every indexed shape", + "search": { + "from": 0, + "size": 10, + "query": { + "geometry": { + "shape": { + "type": "polygon", + "coordinates": [ + [ + [ + -135.0, -38.0 + ], + [ + 149.0, -38.0 + ], + [ + 149.0, 77.0 + ], + [ + -135.0, 77.0 + ] + ] + ] + }, + "relation": "within" + } + }, + "sort": ["-_id"] + }, + "result": { + "total_hits": 9, + "hits": [ + { + "id": "polygon_cubbonpark" + }, + { + "id": "point_museum_of_london" + }, + { + "id": "multipolygon_london_parks" + }, + { + "id": "multipoint_blr_stadiums" + }, + { + "id": "multilinestring_old_airport_road" + }, + { + "id": "linestring_putney_bridge" + }, + { + "id": "geometrycollection_tvm" + }, + { + "id": "envelope_brockwell_park" + }, + { + "id": "circle_halairport" + } + ] + } + } +] diff --git a/test/tests/phrase/data/a.json b/test/tests/phrase/data/a.json new file mode 100644 index 0000000..5fe2dd8 --- /dev/null +++ b/test/tests/phrase/data/a.json @@ -0,0 +1,3 @@ +{ + "body": "Twenty Thousand Leagues Under The Sea" +} \ No newline at end of file diff --git a/test/tests/phrase/data/b.json b/test/tests/phrase/data/b.json new file mode 100644 index 0000000..82aec6e --- /dev/null +++ b/test/tests/phrase/data/b.json @@ -0,0 +1,3 @@ +{ + "body": ["bad call", "defenseless receiver"] +} \ No newline at end of file diff --git a/test/tests/phrase/mapping.json b/test/tests/phrase/mapping.json new file mode 100644 index 0000000..6629a21 --- /dev/null +++ b/test/tests/phrase/mapping.json @@ -0,0 +1,23 @@ +{ + "types": { + "book": { + "properties": { + "body": { + "fields": [ + { + "include_term_vectors": true, + "include_in_all": true, + "index": true, + "store": true, + "analyzer": "en", + "type": "text" + } + ], + "dynamic": true, + "enabled": true + } + } + } + }, + "default_type": "book" +} \ No newline at end of file diff --git a/test/tests/phrase/searches.json b/test/tests/phrase/searches.json new file mode 100644 index 0000000..70f9c83 --- /dev/null +++ b/test/tests/phrase/searches.json @@ -0,0 +1,417 @@ +[ + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Twenty" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Twenty Thousand" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Twenty Thousand Leagues" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Twenty Thousand Leagues Under" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Twenty Thousand Leagues Under the" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Twenty Thousand Leagues Under the Sea" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Thousand" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Thousand Leagues" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Thousand Leagues Under" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Thousand Leagues Under the" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Thousand Leagues Under the Sea" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Leagues" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Leagues Under" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Leagues Under the" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Leagues Under the Sea" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Under the Sea" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "the Sea" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "Sea" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "bad call" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "defenseless receiver" + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "b" + } + ] + } + }, + { + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "match_phrase": "bad receiver" + } + }, + "result": { + "total_hits": 0, + "hits": [] + } + }, + { + "comment": "multi-phrase terms", + "search": { + "from": 0, + "size": 10, + "sort": ["-_score", "_id"], + "query": { + "field": "body", + "terms": [["twenti","thirti"],["thousand"]] + } + }, + "result": { + "total_hits": 1, + "hits": [ + { + "id": "a" + } + ] + } + } +] diff --git a/test/tests/sort/data/a.json b/test/tests/sort/data/a.json new file mode 100644 index 0000000..66ac596 --- /dev/null +++ b/test/tests/sort/data/a.json @@ -0,0 +1,8 @@ +{ + "id": "a", + "name": "marty", + "age": 19, + "born": "2014-11-25", + "title": "mista", + "tags": ["gopher", "belieber"] +} diff --git a/test/tests/sort/data/b.json b/test/tests/sort/data/b.json new file mode 100644 index 0000000..0b84ce8 --- /dev/null +++ b/test/tests/sort/data/b.json @@ -0,0 +1,8 @@ +{ + "id": "b", + "name": "steve", + "age": 21, + "born": "2000-09-11", + "title": "zebra", + "tags": ["thought-leader", "futurist"] +} diff --git a/test/tests/sort/data/c.json b/test/tests/sort/data/c.json new file mode 100644 index 0000000..a1b17b0 --- /dev/null +++ b/test/tests/sort/data/c.json @@ -0,0 +1,8 @@ +{ + "id": "c", + "name": "aster", + "age": 21, + "born": "1954-02-02", + "title": "blogger", + "tags": ["red", "blue", "green"] +} diff --git a/test/tests/sort/data/d.json b/test/tests/sort/data/d.json new file mode 100644 index 0000000..febddca --- /dev/null +++ b/test/tests/sort/data/d.json @@ -0,0 +1,7 @@ +{ + "id": "d", + "age": 65, + "born": "1978-12-02", + "title": "agent d is desperately trying out to be successful rapster!", + "tags": ["cats"] +} diff --git a/test/tests/sort/data/e.json b/test/tests/sort/data/e.json new file mode 100644 index 0000000..9f1c4f9 --- /dev/null +++ b/test/tests/sort/data/e.json @@ -0,0 +1,7 @@ +{ + "id": "e", + "name": "nancy", + "born": "1954-10-22", + "title": "rapstar nancy rapster", + "tags": ["pain"] +} diff --git a/test/tests/sort/data/f.json b/test/tests/sort/data/f.json new file mode 100644 index 0000000..37618bc --- /dev/null +++ b/test/tests/sort/data/f.json @@ -0,0 +1,7 @@ +{ + "id": "f", + "name": "frank", + "age": 1, + "title": "frank the taxman of cb, Rapster!", + "tags": ["vitamin","purple"] +} diff --git a/test/tests/sort/mapping.json b/test/tests/sort/mapping.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/test/tests/sort/mapping.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/test/tests/sort/searches.json b/test/tests/sort/searches.json new file mode 100644 index 0000000..8b06d53 --- /dev/null +++ b/test/tests/sort/searches.json @@ -0,0 +1,554 @@ +[ + { + "comment": "sort by name, ascending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["name"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "c" + }, + { + "id": "f" + }, + { + "id": "a" + }, + { + "id": "e" + }, + { + "id": "b" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "sort by name, descending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["-name"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "b" + }, + { + "id": "e" + }, + { + "id": "a" + }, + { + "id": "f" + }, + { + "id": "c" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "sort by name, descending, missing first", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": [{"by":"field","field":"name","missing":"first","desc":true}] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "d" + }, + { + "id": "b" + }, + { + "id": "e" + }, + { + "id": "a" + }, + { + "id": "f" + }, + { + "id": "c" + } + ] + } + }, + { + "comment": "sort by age, ascending, _id, ascending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["age", "_id"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "f" + }, + { + "id": "a" + }, + { + "id": "b" + }, + { + "id": "c" + }, + { + "id": "d" + }, + { + "id": "e" + } + ] + } + }, + { + "comment": "sort by age, descending, _id, ascending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["-age", "_id"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "d" + }, + { + "id": "b" + }, + { + "id": "c" + }, + { + "id": "a" + }, + { + "id": "f" + }, + { + "id": "e" + } + ] + } + }, + { + "comment": "sort by age, descending, missing first, id, ascending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": [{"by":"field","field":"age","missing":"first","desc":true},{"by":"id","desc":false}] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "e" + }, + { + "id": "d" + }, + { + "id": "b" + }, + { + "id": "c" + }, + { + "id": "a" + }, + { + "id": "f" + } + ] + } + }, + { + "comment": "sort by born, ascending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["born"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "c" + }, + { + "id": "e" + }, + { + "id": "d" + }, + { + "id": "b" + }, + { + "id": "a" + }, + { + "id": "f" + } + ] + } + }, + { + "comment": "sort by born, descending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["-born"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "a" + }, + { + "id": "b" + }, + { + "id": "d" + }, + { + "id": "e" + }, + { + "id": "c" + }, + { + "id": "f" + } + ] + } + }, + { + "comment": "sort by born, descending, missing first", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": [{"by":"field","field":"born","missing":"first","desc":true}] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "f" + }, + { + "id": "a" + }, + { + "id": "b" + }, + { + "id": "d" + }, + { + "id": "e" + }, + { + "id": "c" + } + ] + } + }, + { + "comment": "sort on multi-valued field", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": [{"by":"field","field":"tags","mode":"min"}] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "a" + }, + { + "id": "c" + }, + { + "id": "d" + }, + { + "id": "b" + }, + { + "id": "e" + }, + { + "id": "f" + } + ] + } + }, + { + "comment": "multi-column sort by age, ascending, name, ascending (flips b and c which have same age)", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["age", "name"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "f" + }, + { + "id": "a" + }, + { + "id": "c" + }, + { + "id": "b" + }, + { + "id": "d" + }, + { + "id": "e" + } + ] + } + }, + { + "comment": "sort by docid descending", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["-_id"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "f" + }, + { + "id": "e" + }, + { + "id": "d" + }, + { + "id": "c" + }, + { + "id": "b" + }, + { + "id": "a" + } + ] + } + }, + { + "comment": "sort by name, ascending, after marty", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["name"], + "search_after": ["marty"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "e" + }, + { + "id": "b" + }, + { + "id": "d" + } + ] + } + }, + { + "comment": "sort by name, ascending, before nancy", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["name"], + "search_before": ["nancy"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "c" + }, + { + "id": "f" + }, + { + "id": "a" + } + ] + } + }, + { + "comment": "sort by ID, after doc d", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["_id"], + "search_after": ["d"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "e" + }, + { + "id": "f" + } + ] + } + }, + { + "comment": "sort by ID, before doc d", + "search": { + "from": 0, + "size": 10, + "query": { + "match_all":{} + }, + "sort": ["_id"], + "search_before": ["d"] + }, + "result": { + "total_hits": 6, + "hits": [ + { + "id": "a" + }, + { + "id": "b" + }, + { + "id": "c" + } + ] + } + }, + { + "comment": "sort by score, after score 0.286889[ e(299646) > f(286889) > d(222224)]", + "search": { + "from": 0, + "size": 10, + "query": { + "query":"rapster" + }, + "sort": ["_score"], + "search_after": ["0.286889"] + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "f" + }, + { + "id": "e" + } + ] + } + }, + { + "comment": "sort by score, before score f/0.286889[ e(299646) > f(286889) > d(222224)]", + "search": { + "from": 0, + "size": 10, + "query": { + "query":"rapster" + }, + "sort": ["_score"], + "search_before": ["0.286889"] + }, + "result": { + "total_hits": 3, + "hits": [ + { + "id": "d" + } + ] + } + } +] diff --git a/test/versus_score_test.go b/test/versus_score_test.go new file mode 100644 index 0000000..18c32bf --- /dev/null +++ b/test/versus_score_test.go @@ -0,0 +1,140 @@ +// Copyright (c) 2018 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "os" + "strconv" + "testing" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/document" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" + index "github.com/blevesearch/bleve_index_api" +) + +func TestDisjunctionSearchScoreIndexWithCompositeFields(t *testing.T) { + upHits := disjunctionQueryiOnIndexWithCompositeFields(upsidedown.Name, t) + scHits := disjunctionQueryiOnIndexWithCompositeFields(scorch.Name, t) + + if upHits[0].ID != scHits[0].ID || upHits[1].ID != scHits[1].ID { + t.Errorf("upsidedown, scorch returned different docs;\n"+ + "upsidedown: (%s, %s), scorch: (%s, %s)\n", + upHits[0].ID, upHits[1].ID, scHits[0].ID, scHits[1].ID) + } + + if scHits[0].Score != upHits[0].Score || scHits[1].Score != upHits[1].Score { + t.Errorf("upsidedown, scorch showing different scores;\n"+ + "upsidedown: (%+v, %+v), scorch: (%+v, %+v)\n", + *upHits[0].Expl, *upHits[1].Expl, *scHits[0].Expl, *scHits[1].Expl) + } +} + +func disjunctionQueryiOnIndexWithCompositeFields(indexName string, + t *testing.T, +) []*search.DocumentMatch { + tmpIndexPath, err := os.MkdirTemp("", "bleve-testidx") + if err != nil { + t.Fatalf("error creating temp dir: %v", err) + } + defer func() { + err := os.RemoveAll(tmpIndexPath) + if err != nil { + t.Fatalf("error removing temp dir: %v", err) + } + }() + // create an index + idxMapping := mapping.NewIndexMapping() + idx, err := bleve.NewUsing(tmpIndexPath, idxMapping, indexName, + bleve.Config.DefaultKVStore, nil) + if err != nil { + t.Error(err) + } + + defer func() { + err = idx.Close() + if err != nil { + t.Error(err) + } + }() + + // create and insert documents as a batch + batch := idx.NewBatch() + docs := []struct { + field1 string + field2 int + }{ + { + field1: "one", + field2: 1, + }, + { + field1: "two", + field2: 2, + }, + } + + for i := 0; i < len(docs); i++ { + doc := document.NewDocument(strconv.Itoa(docs[i].field2)) + doc.Fields = []document.Field{ + document.NewTextField("field1", []uint64{}, []byte(docs[i].field1)), + document.NewNumericField("field2", []uint64{}, float64(docs[i].field2)), + } + doc.CompositeFields = []*document.CompositeField{ + document.NewCompositeFieldWithIndexingOptions( + "_all", true, []string{"field1"}, []string{}, + index.IndexField|index.IncludeTermVectors), + } + if err = batch.IndexAdvanced(doc); err != nil { + t.Error(err) + } + } + if err = idx.Batch(batch); err != nil { + t.Error(err) + } + + /* + Query: + DISJ + / \ + CONJ TERM(two) + / + TERM(one) + */ + + tq1 := bleve.NewTermQuery("one") + tq1.SetBoost(2) + tq2 := bleve.NewTermQuery("two") + tq2.SetBoost(3) + + cq := bleve.NewConjunctionQuery(tq1) + cq.SetBoost(4) + + q := bleve.NewDisjunctionQuery(tq1, tq2) + sr := bleve.NewSearchRequestOptions(q, 2, 0, true) + res, err := idx.Search(sr) + if err != nil { + t.Error(err) + } + + if len(res.Hits) != 2 { + t.Errorf("indexType: %s Expected 2 hits, but got: %v", indexName, len(res.Hits)) + } + + return res.Hits +} diff --git a/test/versus_test.go b/test/versus_test.go new file mode 100644 index 0000000..119c626 --- /dev/null +++ b/test/versus_test.go @@ -0,0 +1,519 @@ +// Copyright (c) 2014 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "math/rand" + "os" + "reflect" + "strconv" + "strings" + "testing" + "text/template" + "time" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/index/scorch" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/blevesearch/bleve/v2/index/upsidedown/store/boltdb" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/blevesearch/bleve/v2/search" +) + +// Tests scorch indexer versus upsidedown/bolt indexer against various +// templated queries. Example usage from the bleve top-level directory... +// +// go test -v -run TestScorchVersusUpsideDownBolt ./test +// VERBOSE=1 FOCUS=Trista go test -v -run TestScorchVersusUpsideDownBolt ./test +// + +func init() { + // override for tests + scorch.DefaultPersisterNapTimeMSec = 1 +} + +func TestScorchVersusUpsideDownBoltAll(t *testing.T) { + (&VersusTest{ + t: t, + NumDocs: 1000, + MaxWordsPerDoc: 20, + NumWords: 10, + BatchSize: 1000, + NumAttemptsPerSearch: 100, + }).run(scorch.Name, boltdb.Name, upsidedown.Name, boltdb.Name, nil, nil) +} + +func TestScorchVersusUpsideDownBoltSmallMNSAM(t *testing.T) { + (&VersusTest{ + t: t, + Focus: "must-not-same-as-must", + NumDocs: 5, + MaxWordsPerDoc: 2, + NumWords: 1, + BatchSize: 1, + NumAttemptsPerSearch: 1, + }).run(scorch.Name, boltdb.Name, upsidedown.Name, boltdb.Name, nil, nil) +} + +func TestScorchVersusUpsideDownBoltSmallCMP11(t *testing.T) { + (&VersusTest{ + t: t, + Focus: "conjuncts-match-phrase-1-1", + NumDocs: 30, + MaxWordsPerDoc: 8, + NumWords: 2, + BatchSize: 1, + NumAttemptsPerSearch: 1, + }).run(scorch.Name, boltdb.Name, upsidedown.Name, boltdb.Name, nil, nil) +} + +// ------------------------------------------------------- + +// Templates used to compare search results in the "versus" tests. +var testVersusSearchTemplates = []string{ + `{ + "about": "expected to return zero hits", + "query": { + "query": "title:notARealTitle" + } + }`, + `{ + "about": "try straight word()'s", + "query": { + "query": "body:{{word}}" + } + }`, + `{ + "about": "conjuncts on same term", + "query": { + "conjuncts": [ + { "field": "body", "term": "{{word}}", "boost": 1.0 }, + { "field": "body", "term": "{{word}}", "boost": 1.0 } + ] + } + }`, + `{ + "about": "disjuncts on same term", + "query": { + "disjuncts": [ + { "field": "body", "term": "{{word}}", "boost": 1.0 }, + { "field": "body", "term": "{{word}}", "boost": 1.0 } + ] + } + }`, + `{ + "about": "never-matching-title-conjuncts", + "query": { + "conjuncts": [ + {"field": "body", "match": "{{word}}"}, + {"field": "body", "match": "{{word}}"}, + {"field": "title", "match": "notAnActualTitle"} + ] + } + }`, + `{ + "about": "never-matching-title-disjuncts", + "query": { + "disjuncts": [ + {"field": "body", "match": "{{word}}"}, + {"field": "body", "match": "{{word}}"}, + {"field": "title", "match": "notAnActualTitle"} + ] + } + }`, + `{ + "about": "must-not-never-matches", + "query": { + "must_not": {"disjuncts": [ + {"field": "title", "match": "notAnActualTitle"} + ]}, + "should": {"disjuncts": [ + {"field": "body", "match": "{{word}}"} + ]} + } + }`, + `{ + "about": "must-not-only", + "query": { + "must_not": {"disjuncts": [ + {"field": "body", "term": "{{word}}"} + ]} + } + }`, + `{ + "about": "must-not-same-as-must -- see: MB-27291", + "query": { + "must_not": {"disjuncts": [ + {"field": "body", "match": "{{word}}"} + ]}, + "must": {"conjuncts": [ + {"field": "body", "match": "{{word}}"} + ]} + } + }`, + `{ + "about": "must-not-same-as-should", + "query": { + "must_not": {"disjuncts": [ + {"field": "body", "match": "{{word}}"} + ]}, + "should": {"disjuncts": [ + {"field": "body", "match": "{{word}}"} + ]} + } + }`, + `{ + "about": "inspired by testrunner RQG issue -- see: MB-27291", + "query": { + "must_not": {"disjuncts": [ + {"field": "title", "match": "Trista Allen"}, + {"field": "body", "match": "{{word}}"} + ]}, + "should": {"disjuncts": [ + {"field": "title", "match": "Kallie Safiya Amara"}, + {"field": "body", "match": "{{word}}"} + ]} + } + }`, + `{ + "about": "conjuncts-match-phrase-1-1 inspired by testrunner RQG issue -- see: MB-27291", + "query": { + "conjuncts": [ + {"field": "body", "match": "{{bodyWord 0}}"}, + {"field": "body", "match_phrase": "{{bodyWord 1}} {{bodyWord 1}}"} + ] + } + }`, + `{ + "about": "conjuncts-match-phrase-1-2 inspired by testrunner RQG issue -- see: MB-27291 -- FAILS!!", + "query": { + "conjuncts": [ + {"field": "body", "match": "{{bodyWord 0}}"}, + {"field": "body", "match_phrase": "{{bodyWord 1}} {{bodyWord 2}}"} + ] + } + }`, +} + +// ------------------------------------------------------- + +type VersusTest struct { + t *testing.T + + // Use environment variable VERBOSE= that's > 0 for more + // verbose output. + Verbose int + + // Allow user to focus on particular search templates, where + // where the search template must contain the Focus string. + Focus string + + NumDocs int // Number of docs to insert. + MaxWordsPerDoc int // Max number words in each doc's Body field. + NumWords int // Total number of words in the dictionary. + BatchSize int // Batch size when inserting docs. + NumAttemptsPerSearch int // For each search template, number of searches to try. + + // The Bodies is an array with length NumDocs, where each entry + // is the words in a doc's Body field. + Bodies [][]string + + CurAttempt int + TotAttempts int +} + +// ------------------------------------------------------- + +func testVersusSearches(vt *VersusTest, searchTemplates []string, idxA, idxB bleve.Index) { + t := vt.t + + funcMap := template.FuncMap{ + // Returns a word. The word may or may not be in any + // document's body. + "word": func() string { + return vt.genWord(vt.CurAttempt % vt.NumWords) + }, + // Picks a document and returns the i'th word in that + // document's body. You can use this in searches to + // definitely find at least one document. + "bodyWord": func(i int) string { + body := vt.Bodies[vt.CurAttempt%len(vt.Bodies)] + if len(body) == 0 { + return "" + } + return body[i%len(body)] + }, + } + + // Optionally allow call to focus on a particular search templates, + // where the search template must contain the vt.Focus string. + if vt.Focus == "" { + vt.Focus = os.Getenv("FOCUS") + } + + for i, searchTemplate := range searchTemplates { + if vt.Focus != "" && !strings.Contains(searchTemplate, vt.Focus) { + continue + } + + tmpl, err := template.New("search").Funcs(funcMap).Parse(searchTemplate) + if err != nil { + t.Fatalf("could not parse search template: %s, err: %v", searchTemplate, err) + } + for j := 0; j < vt.NumAttemptsPerSearch; j++ { + vt.CurAttempt = j + + var buf bytes.Buffer + err = tmpl.Execute(&buf, vt) + if err != nil { + t.Fatalf("could not execute search template: %s, err: %v", searchTemplate, err) + } + + bufBytes := buf.Bytes() + + if vt.Verbose > 0 { + fmt.Printf(" %s\n", bufBytes) + } + + var search bleve.SearchRequest + err = json.Unmarshal(bufBytes, &search) + if err != nil { + t.Fatalf("could not unmarshal search: %s, err: %v", bufBytes, err) + } + + search.Size = vt.NumDocs * 10 // Crank up limit to get all results. + + searchA := search + searchB := search + + resA, errA := idxA.Search(&searchA) + resB, errB := idxB.Search(&searchB) + if errA != errB { + t.Errorf("search: (%d) %s,\n err mismatch, errA: %v, errB: %v", + i, bufBytes, errA, errB) + } + + // Scores might have float64 vs float32 wobbles, so truncate precision. + resA.MaxScore = math.Trunc(resA.MaxScore*1000.0) / 1000.0 + resB.MaxScore = math.Trunc(resB.MaxScore*1000.0) / 1000.0 + + // Timings may be different between A & B, so force equality. + resA.Took = resB.Took + + // Hits might have different ordering since some indexers + // (like upsidedown) have a natural secondary sort on id + // while others (like scorch) don't. So, we compare by + // putting the hits from A & B into maps. + hitsA := hitsById(resA) + hitsB := hitsById(resB) + for id, hitA := range hitsA { + hitB := hitsB[id] + if len(hitA.FieldTermLocations) == 0 { + hitA.FieldTermLocations = nil + } + if len(hitB.FieldTermLocations) == 0 { + hitB.FieldTermLocations = nil + } + if !reflect.DeepEqual(hitA, hitB) { + t.Errorf("\n driving from hitsA\n hitA: %#v,\n hitB: %#v", hitA, hitB) + idx, _ := strconv.Atoi(id) + t.Errorf("\n doc: %d, body: %s", idx, strings.Join(vt.Bodies[idx], " ")) + } + } + for id, hitB := range hitsB { + hitA := hitsA[id] + if len(hitA.FieldTermLocations) == 0 { + hitA.FieldTermLocations = nil + } + if len(hitB.FieldTermLocations) == 0 { + hitB.FieldTermLocations = nil + } + if !reflect.DeepEqual(hitA, hitB) { + t.Errorf("\n driving from hitsB\n hitA: %#v,\n hitB: %#v", hitA, hitB) + idx, _ := strconv.Atoi(id) + t.Errorf("\n doc: %d, body: %s", idx, strings.Join(vt.Bodies[idx], " ")) + } + } + + if !reflect.DeepEqual(hitsA, hitsB) { + t.Errorf("=========\nsearch: (%d) %s,\n res hits mismatch,\n len(hitsA): %d,\n len(hitsB): %d", + i, bufBytes, len(hitsA), len(hitsB)) + t.Errorf("\n hitsA: %#v,\n hitsB: %#v", + hitsA, hitsB) + } + + resA.Hits = nil + resB.Hits = nil + resA.Cost = 0 + resB.Cost = 0 + + if !reflect.DeepEqual(resA, resB) { + resAj, _ := json.Marshal(resA) + resBj, _ := json.Marshal(resB) + t.Errorf("search: (%d) %s,\n res mismatch,\n resA: %s,\n resB: %s", + i, bufBytes, resAj, resBj) + } + + if vt.Verbose > 0 { + fmt.Printf(" Total: (%t) %d\n", resA.Total == resB.Total, resA.Total) + } + + vt.TotAttempts++ + } + } +} + +// Organizes the hits into a map keyed by id. +func hitsById(res *bleve.SearchResult) map[string]*search.DocumentMatch { + rv := make(map[string]*search.DocumentMatch, len(res.Hits)) + + for _, hit := range res.Hits { + // Clear out or truncate precision of hit fields that might be + // different across different indexer implementations. + hit.Index = "" + hit.Score = math.Trunc(hit.Score*1000.0) / 1000.0 + hit.IndexInternalID = nil + hit.HitNumber = 0 + + rv[hit.ID] = hit + } + + return rv +} + +// ------------------------------------------------------- + +func (vt *VersusTest) run(indexTypeA, kvStoreA, indexTypeB, kvStoreB string, + cb func(versusTest *VersusTest, searchTemplates []string, idxA, idxB bleve.Index), + searchTemplates []string, +) { + if cb == nil { + cb = testVersusSearches + } + + if searchTemplates == nil { + searchTemplates = testVersusSearchTemplates + } + + if vt.Verbose <= 0 { + vt.Verbose, _ = strconv.Atoi(os.Getenv("VERBOSE")) + } + + dirA := "/tmp/bleve-versus-test-a" + dirB := "/tmp/bleve-versus-test-b" + + defer func() { + _ = os.RemoveAll(dirA) + _ = os.RemoveAll(dirB) + }() + + _ = os.RemoveAll(dirA) + _ = os.RemoveAll(dirB) + + imA := vt.makeIndexMapping() + imB := vt.makeIndexMapping() + + kvConfigA := map[string]interface{}{} + kvConfigB := map[string]interface{}{} + + idxA, err := bleve.NewUsing(dirA, imA, indexTypeA, kvStoreA, kvConfigA) + if err != nil || idxA == nil { + vt.t.Fatalf("new using err: %v", err) + } + defer func() { _ = idxA.Close() }() + + idxB, err := bleve.NewUsing(dirB, imB, indexTypeB, kvStoreB, kvConfigB) + if err != nil || idxB == nil { + vt.t.Fatalf("new using err: %v", err) + } + defer func() { _ = idxB.Close() }() + + if vt.Bodies == nil { + vt.Bodies = vt.genBodies() + } + + vt.insertBodies(idxA) + vt.insertBodies(idxB) + + cb(vt, searchTemplates, idxA, idxB) +} + +// ------------------------------------------------------- + +func (vt *VersusTest) makeIndexMapping() mapping.IndexMapping { + standardFM := bleve.NewTextFieldMapping() + standardFM.Store = false + standardFM.IncludeInAll = false + standardFM.IncludeTermVectors = true + standardFM.Analyzer = "standard" + + dm := bleve.NewDocumentMapping() + dm.AddFieldMappingsAt("title", standardFM) + dm.AddFieldMappingsAt("body", standardFM) + + im := bleve.NewIndexMapping() + im.DefaultMapping = dm + im.DefaultAnalyzer = "standard" + + return im +} + +func (vt *VersusTest) insertBodies(idx bleve.Index) { + batch := idx.NewBatch() + for i, bodyWords := range vt.Bodies { + title := fmt.Sprintf("%d", i) + body := strings.Join(bodyWords, " ") + err := batch.Index(title, map[string]interface{}{"title": title, "body": body}) + if err != nil { + vt.t.Fatalf("batch.Index err: %v", err) + } + if i%vt.BatchSize == 0 { + err = idx.Batch(batch) + if err != nil { + vt.t.Fatalf("batch err: %v", err) + } + batch.Reset() + } + } + err := idx.Batch(batch) + if err != nil { + vt.t.Fatalf("last batch err: %v", err) + } +} + +func (vt *VersusTest) genBodies() (rv [][]string) { + for i := 0; i < vt.NumDocs; i++ { + rv = append(rv, vt.genBody()) + } + return rv +} + +func (vt *VersusTest) genBody() (rv []string) { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + m := rng.Intn(vt.MaxWordsPerDoc) + for j := 0; j < m; j++ { + rv = append(rv, vt.genWord(rng.Intn(vt.NumWords))) + } + return rv +} + +func (vt *VersusTest) genWord(i int) string { + return fmt.Sprintf("%x", i) +} diff --git a/util/extract.go b/util/extract.go new file mode 100644 index 0000000..0d3decf --- /dev/null +++ b/util/extract.go @@ -0,0 +1,66 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "math" + "reflect" +) + +// extract numeric value (if possible) and returns a float64 +func ExtractNumericValFloat64(v interface{}) (float64, bool) { + val := reflect.ValueOf(v) + if !val.IsValid() { + return 0, false + } + + switch { + case val.CanFloat(): + return val.Float(), true + case val.CanInt(): + return float64(val.Int()), true + case val.CanUint(): + return float64(val.Uint()), true + } + + return 0, false +} + +// extract numeric value (if possible) and returns a float32 +func ExtractNumericValFloat32(v interface{}) (float32, bool) { + val := reflect.ValueOf(v) + if !val.IsValid() { + return 0, false + } + + switch { + case val.CanFloat(): + floatVal := val.Float() + if !IsValidFloat32(floatVal) { + return 0, false + } + return float32(floatVal), true + case val.CanInt(): + return float32(val.Int()), true + case val.CanUint(): + return float32(val.Uint()), true + } + + return 0, false +} + +func IsValidFloat32(val float64) bool { + return !math.IsNaN(val) && !math.IsInf(val, 0) && val <= math.MaxFloat32 +} diff --git a/util/json.go b/util/json.go new file mode 100644 index 0000000..8745dd8 --- /dev/null +++ b/util/json.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Couchbase, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "encoding/json" +) + +// Should only be overwritten during process init()'ialization. +var ( + MarshalJSON = json.Marshal + UnmarshalJSON = json.Unmarshal +)