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 +7 -0
- data/README.md +44 -0
- data/db/migrate/20200302171011_add_fulltext_indexing.rb +11 -0
- data/lib/card/query/card_query/full_text_matching.rb +67 -0
- data/set/abstract/filter.rb +27 -0
- data/set/abstract/no_search_content.rb +3 -0
- data/set/abstract/search_content_fields.rb +15 -0
- data/set/all/search_content.rb +45 -0
- metadata +66 -0
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,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: []
|