card-mod-fulltext 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 93a52bd07ccc0265fd84b336b56756dd8a4ef2aa2b8e5152b4f44e4f453b2492
4
+ data.tar.gz: def54ecc50932efc833d39b2f6657c5c67ba3f18d28ee9039b17fb26a21f207c
5
+ SHA512:
6
+ metadata.gz: 005e152195747735d7dcda0622790d165bcc6564cbc1174efcce62651721005de2cf42669df721c2b8bc38ee7deb78ea0d3dc12a6e357e3c8c28427abba8ef95
7
+ data.tar.gz: fa89c31f7e9581f5441328c52552685f6153e29308a279953a477ab712f85faf6a12aee121eb12f2e692fdd2ae2814b89d9c319a1542854da90c362957b1dc03
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ <!--
2
+ # @title README - mod: fulltext
3
+ -->
4
+
5
+ # Fulltext mod
6
+
7
+ This mod adds MySQL fulltext support for card searches.
8
+
9
+ It does so by adding a `search_content` field to the cards table and indexing the
10
+ `name` and `search_content` fields.
11
+
12
+ For simple cards, the `search_content` simply duplicates the `db_content` field (which
13
+ contains default card content). But the fulltext search becomes much more powerful if
14
+ the `search_content` is customized for different sets, often by including the content of
15
+ select related cards.
16
+
17
+ ## CQL
18
+
19
+ This mod adds support for fulltext matching in card queries, eg:
20
+
21
+ fulltext_match: "MYKEYWORD"
22
+
23
+ Alternatively, you can trigger a fulltext match using a `:` prefix with a standard
24
+ match statement.
25
+
26
+ match: ":MYKEYWORD"
27
+
28
+ It also adds support for relevance sorting:
29
+
30
+ sort: "relevance"
31
+
32
+ ## Sets
33
+
34
+ ### Abstract::SearchContentFields
35
+
36
+ After including this set, you can add a `#search_content_field_codes` method that returns
37
+ a list of codenames. The `#search_content` field will be populated by concatenating
38
+ the content of those fields.
39
+
40
+ ### Abstract::NoSearchContent
41
+
42
+ With this set, the `search_content` will be blank (and fulltext matching will be use
43
+ only the name field)
44
+
@@ -0,0 +1,11 @@
1
+ class AddFulltextIndexing < ActiveRecord::Migration[6.0]
2
+ def up
3
+ add_column :cards, :search_content, :text, limit: 1.megabyte
4
+ add_index :cards, %i[name search_content],
5
+ name: "name, search_content_index", type: :fulltext
6
+ end
7
+
8
+ def down
9
+ remove_column :cards, :search_content
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+
2
+ class Card
3
+ module Query
4
+ attributes[:fulltext_match] = :relational
5
+
6
+ class CardQuery
7
+ # handle `fulltext_match` condition in card queries
8
+ module FullTextMatching
9
+ def fulltext_match value
10
+ return if value.strip.empty?
11
+ if prefixed_match? value
12
+ name_match value
13
+ else
14
+ add_condition Value.new([:match, ":#{value}"], self).to_sql(:name)
15
+ end
16
+ end
17
+
18
+ def prefixed_match? value
19
+ value.match?(/^[\~\:\=]/)
20
+ end
21
+ end
22
+ end
23
+
24
+ class SqlStatement
25
+ # handle `fulltext_match` relevance sorting in card queries
26
+ module FullTextOrdering
27
+ # when there is full text matching in the where clause,
28
+ # the default ordering is by relevance, so we just need to
29
+ # make sure there is no explicit order by clause
30
+ def order
31
+ super unless order_config == "relevance"
32
+ end
33
+
34
+ # Note: the more explicit route (which will be necessary if we want to support
35
+ # relevance as one of multiple sort options), is to do something like
36
+ # select MATCH (x) AGAINST (y) as relevance... order by relevance desc
37
+ end
38
+ include FullTextOrdering
39
+ end
40
+
41
+ class Value
42
+ def self.match_prefices
43
+ @match_prefices ||= %w[= ~ :]
44
+ end
45
+
46
+ module FullTextValue
47
+ def match_sql _field
48
+ return super unless fulltext_match_term?
49
+ "MATCH (#{fulltext_fields}) AGAINST (#{quote match_term} #{fulltext_mode})"
50
+ end
51
+
52
+ def fulltext_match_term?
53
+ match_prefix.match? ":"
54
+ end
55
+
56
+ def fulltext_fields
57
+ %i[search_content name].map { |fld| "#{@query.table_alias}.#{fld}" }.join ", "
58
+ end
59
+
60
+ def fulltext_mode
61
+ match_prefix == "::" ? "IN BOOLEAN MODE" : ""
62
+ end
63
+ end
64
+ include FullTextValue
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,27 @@
1
+ format do
2
+ def sort_options
3
+ handling_fulltext do
4
+ # TODO: make it possible to use super here
5
+ { "Alphabetical": :name, "Recently Added": :create }
6
+ end
7
+ end
8
+
9
+ private
10
+ # This only shows relevance sorting when the page is loaded with an explicit
11
+ # fulltext prefix (:) in the name search. Otherwise "relevance" sorting doesn't
12
+ # make sense.
13
+ def handling_fulltext
14
+ options = yield
15
+ add_sort_by_relevance options if fulltext_name_filtering?
16
+ options
17
+ end
18
+
19
+ def add_sort_by_relevance options
20
+ options["Relevance"] = :relevance
21
+ params[:sort] = :relevance unless sort_param
22
+ end
23
+
24
+ def fulltext_name_filtering?
25
+ params.dig(:filter, :name)&.match?(/^\:/)
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ def content_for_search
2
+ nil
3
+ end
@@ -0,0 +1,15 @@
1
+ def content_for_search
2
+ content_for_search_from_fields
3
+ end
4
+
5
+ def search_content_cards
6
+ search_content_field_codes.map do |code|
7
+ fetch code, new: {}
8
+ end.compact
9
+ end
10
+
11
+ def search_content_field_names
12
+ search_content_field_codes.map do |code|
13
+ name.trait_name code
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ event :set_search_content, after: :set_content do
2
+ self.search_content = tagless { content_for_search.to_s }
3
+ end
4
+
5
+ event :trigger_left_search_content_update, after: :set_search_content do
6
+ return unless name.compound?
7
+ l = left
8
+
9
+ return unless l&.real? && l.search_content_field_names&.include?(name)
10
+
11
+ l.set_search_content
12
+ l.save if l.search_content_changed?
13
+ end
14
+
15
+ def tagless
16
+ ::ActionView::Base.full_sanitizer.sanitize yield
17
+ end
18
+
19
+ def name_for_search
20
+ name
21
+ end
22
+
23
+ def content_for_search
24
+ structure ? content_for_search_from_fields : content
25
+ end
26
+
27
+ def search_content_cards
28
+ return [] unless structure && nest_chunks
29
+
30
+ nest_chunks.map(&:referee_card).compact
31
+ end
32
+
33
+ def search_content_field_names
34
+ search_content_cards.map(&:name)
35
+ end
36
+
37
+ def content_for_search_from_fields
38
+ search_content_cards.map(&:content).compact.join "\n"
39
+ end
40
+
41
+ format do
42
+ view :search_content do
43
+ card.search_content
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: card-mod-fulltext
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Philipp Kühl
8
+ - Ethan McCutchen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2021-10-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: card
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ description: ''
29
+ email:
30
+ - info@decko.org
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - README.md
36
+ - db/migrate/20200302171011_add_fulltext_indexing.rb
37
+ - lib/card/query/card_query/full_text_matching.rb
38
+ - set/abstract/filter.rb
39
+ - set/abstract/no_search_content.rb
40
+ - set/abstract/search_content_fields.rb
41
+ - set/all/search_content.rb
42
+ homepage: http://decko.org
43
+ licenses:
44
+ - GPL-3.0
45
+ metadata:
46
+ card-mod: fulltext
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '2.5'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.2.28
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: MySQL fulltext searches for decko
66
+ test_files: []