Compare commits

..

39 Commits

Author SHA1 Message Date
mueh e4590f3ea9 Merge branch 'main' of https://gitea.muehlberger.net/sbp-jm/sbp-flarum-search 2026-04-28 15:40:42 +02:00
mueh 83c0b0e7ec Working stable release! 2026-04-28 15:40:09 +02:00
MauBiBot 3a8559ecca Rename internal API route reference from blomstra.search to sbp.search 2026-04-28 14:46:44 +02:00
MauBiBot 082892f413 Fix forum search API endpoint to /sbp/search 2026-04-28 11:51:34 +02:00
MauBiBot 0b9586aee2 Fix forum search API endpoint to /sbp/search 2026-04-28 11:51:15 +02:00
mueh c54d916361 richitger api pfad 2026-04-28 11:49:57 +02:00
jm 09c01b3445 glaub nicht dass das böse war 2026-04-28 09:30:40 +00:00
jm a089698247 register provider evtl. böse? 2026-04-28 09:25:30 +00:00
mueh 7344fc33b7 revert to old extend.php 2026-04-28 11:06:47 +02:00
jm 2696fa4dfd wieder ohne extensionid 2026-04-28 09:00:51 +00:00
mueh a6d6230553 typo 2026-04-28 10:57:15 +02:00
mueh f537506166 Merge branch 'main' of https://gitea.muehlberger.net/sbp-jm/sbp-flarum-search 2026-04-28 10:49:51 +02:00
mueh a9bb20dfe7 extension id angegeben 2026-04-28 10:49:22 +02:00
MauBiBot 6417e8e9f4 Bind admin settings to sbp-search extension page 2026-04-28 10:31:54 +02:00
mueh 22e2796a65 admin js hatte noch alte blomstra-search drin 2026-04-28 10:27:28 +02:00
mueh 52168bb5c3 js wieder gefixt aus anderem commit 2026-04-28 10:01:50 +02:00
mueh 7f4befd73b composer fix 2026-04-28 09:37:39 +02:00
mueh 115c06e9bf Merge branch 'sbp-hardfork' 2026-04-28 09:07:16 +02:00
jm d2b8869eb4 composer update 2026-04-22 22:01:57 +00:00
MauBiBot f72acc60ef Rename blomstra/search fork to sbp/flarum-search (Phase 1: namespaces, routes, JS initializers) 2026-04-22 22:23:47 +02:00
JM fc830ea921 Add content autocomplete to search dropdown 2026-04-22 20:52:35 +02:00
MauBiBot 4538fe2ba8 Add prefix search for content and enable autocomplete 2026-04-22 20:43:33 +02:00
JM 5774879331 fuzzy search in thread 2026-04-22 20:19:09 +02:00
MauBiBot 3e2f3f9585 always use ES search on initial search page load 2026-04-22 19:46:09 +02:00
MauBiBot bf90e3279e remove frontend default sort from discussion search state 2026-04-22 19:31:30 +02:00
MauBiBot bce3258325 fix forum search route and rebuild compiled assets 2026-04-22 19:05:46 +02:00
MauBiBot 7f282cdb84 merge local search fixes after pull 2026-04-22 18:56:43 +02:00
JM 7cb4071a54 restore strong title match and keep prefix title fallback 2026-04-22 18:08:12 +02:00
JM e3dfc04192 add low-boost title autocomplete match to full search 2026-04-22 17:50:30 +02:00
MauBiBot e7e4495306 rebuild compiled JS assets for analyzer selector 2026-04-22 16:36:24 +02:00
mueh 0fa8fb92eb ai forgot to commit 2026-04-22 15:47:54 +02:00
mueh 925ecb0e7c tune title autocomplete edge_ngram bounds 2026-04-22 12:33:13 +02:00
Bart van Bragt e92307cf16 build: commit compiled JS dist files for feat/analyzer-refactor branch 2026-04-16 22:07:28 +02:00
Bart van Bragt d27d71ff72 style: apply prettier formatting to admin/index.ts 2026-04-16 11:57:59 +02:00
Bart van Bragt f7d9935fb9 feat: suppress ES instant results when search-post-bodies is enabled
When searching inside comments is ON, the autocomplete dropdown now returns
nothing from ES (only usernames show). This avoids showing title-only matches
that are inconsistent with the full comment-body search results.

Also passes filter[autocomplete]=1 to the API so the server uses the fast
title.autocomplete path instead of the full has_child query.
2026-04-16 11:57:34 +02:00
Bart van Bragt 33d4946947 feat: stem exclusion admin setting and analyzer language selector improvements
Add a stem-exclusion textarea to the admin settings page. Words entered here
(one per line) are passed as stem_exclusion to the language analyzer, preventing
them from being stemmed. Requires a full re-index to take effect.

The two language-dependent fields (stem exclusion, min search length) are
disabled when CJK is selected, since neither applies to bigram analysis.

Settings are reordered: search-scope switches now appear above the language
block so the language-dependent fields sit directly below the language selector.

Replace the lowercase ES identifier labels in the language dropdown with proper
display names (e.g. "Brazilian Portuguese", "CJK (Chinese, Japanese, Korean)",
"Sorani (Kurdish)"). Drift detection now tracks stem exclusion instead of the
removed min_gram index setting.
2026-04-16 11:52:34 +02:00
Bart van Bragt 0f58abf141 feat: add title.autocomplete instant search path sorted by latest
When filter[autocomplete]=1 is present, bypass the full has_child query
and run a single MatchQuery on title.autocomplete (edge-ngram prefix match).
Results are sorted by updated_at desc to stay consistent with full search.
2026-04-16 11:51:45 +02:00
Bart van Bragt 0adaab5d84 fix: resolve analyzer mismatch with symmetric language analyzer and title field
Replace the asymmetric ngram (index) / language (search) setup on content
with a symmetric language analyzer at both index and search time, eliminating
zero-hit cases caused by stemmed tokens falling below min_gram (e.g. Turkish).

Add a title field to discussion documents with a title.autocomplete edge-ngram
subfield (min=2, max=15) for prefix instant search, and a flarum_title_search
analyzer (lowercase only) as its search-time counterpart.

Remove flarum_analyzer_partial / partial_search_filter / search_analyzer from
the content mapping — no longer needed with symmetric analysis.

Bump INDEX_COMPAT_VERSION to v3. Also fix MatchQuery::toArray() to serialize
the analyzer property which was stored but never emitted.
2026-04-16 11:51:28 +02:00
flarum-bot bf19eed20f Bundled output for commit 2fc66b9bf7
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2026-04-13 20:22:53 +00:00
38 changed files with 424 additions and 180 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ js/node_modules
js/dist js/dist
vendor/ vendor/
composer.lock composer.lock
analysis/*

View File

@ -18,9 +18,32 @@ fulltext search with one that is completely relying on the proven elasticsearch
## Installation ## Installation
```bash ```bash
composer require blomstra/search:* composer require sbp/flarum-search
``` ```
## Notes
This fork is now maintained as `sbp/flarum-search`.
Intentional compatibility leftovers:
- settings keys remain `blomstra-search.*`
- some translation keys remain `blomstra-search.*`
- CLI command names may still remain `blomstra:*` until a later migration
Search behavior:
- title autocomplete enabled
- fuzzy title search enabled
- exact + fuzzy content search on full search page
- no content autocomplete in dropdown (performance decision)
``
Repository can be found here: https://gitea.muehlberger.net/sbp-jm/sbp-flarum-search
Enable the extension in the admin area and configure the Elasticsearch endpoint and index name in the extension settings. Enable the extension in the admin area and configure the Elasticsearch endpoint and index name in the extension settings.
## Setting up the index ## Setting up the index

View File

@ -1,5 +1,5 @@
{ {
"name": "blomstra/search", "name": "sbp/flarum-search",
"description": "Replaces Flarum search with one powered by an elastic search server.", "description": "Replaces Flarum search with one powered by an elastic search server.",
"keywords": [ "keywords": [
"extension", "extension",
@ -11,16 +11,20 @@
"elasticsearch" "elasticsearch"
], ],
"support": { "support": {
"email": "helpdesk@blomstra.net", "email": "mueh+git@sbp.de",
"forum": "https://blomstra.community/t/ext-search" "forum": "https://blomstra.community/t/ext-search"
}, },
"type": "flarum-extension", "type": "flarum-extension",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{
"name": "sbp",
"email": "info@sbp.de"
},
{ {
"name": "Team Blomstra", "name": "Team Blomstra",
"email": "development@blomstra.net", "email": "development@blomstra.net",
"role": "Developer" "role": "Original Developer"
} }
], ],
"require": { "require": {
@ -43,7 +47,7 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Blomstra\\Search\\": "src/" "Sbp\\Search\\": "src/"
} }
} }
} }

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search; namespace Sbp\Search;
use Flarum\Extend as Flarum; use Flarum\Extend as Flarum;
@ -26,8 +26,8 @@ return [
new Flarum\Locales(__DIR__.'/resources/locale'), new Flarum\Locales(__DIR__.'/resources/locale'),
(new Flarum\Routes('api')) (new Flarum\Routes('api'))
->get('/blomstra/search/{type}', 'blomstra.search', Api\Controllers\SearchController::class) ->get('/sbp/search/{type}', 'sbp.search', Api\Controllers\SearchController::class)
->put('/blomstra/search/index', 'blomstra.search.index', Api\Controllers\IndexController::class), ->put('/sbp/search/index', 'sbp.search.index', Api\Controllers\IndexController::class),
(new Flarum\Console()) (new Flarum\Console())
->command(Commands\BuildCommand::class), ->command(Commands\BuildCommand::class),
@ -35,5 +35,7 @@ return [
->default('blomstra-search.search-discussion-subjects', true) ->default('blomstra-search.search-discussion-subjects', true)
->default('blomstra-search.search-post-bodies', true) ->default('blomstra-search.search-post-bodies', true)
->default('blomstra-search.min-search-length', Commands\BuildCommand::DEFAULT_MIN_SEARCH_LENGTH) ->default('blomstra-search.min-search-length', Commands\BuildCommand::DEFAULT_MIN_SEARCH_LENGTH)
->serializeToForum('blomstraSearchMinLength', 'blomstra-search.min-search-length', 'intval'), ->default('blomstra-search.stem-exclusion', '')
->serializeToForum('blomstraSearchMinLength', 'blomstra-search.min-search-length', 'intval')
->serializeToForum('blomstraSearchPostBodies', 'blomstra-search.search-post-bodies', fn ($v) => (bool) $v, true),
]; ];

2
js/dist/admin.js vendored
View File

@ -1,2 +1,2 @@
(()=>{var t={n:a=>{var e=a&&a.__esModule?()=>a.default:()=>a;return t.d(e,{a:e}),e},d:(a,e)=>{for(var r in e)t.o(e,r)&&!t.o(a,r)&&Object.defineProperty(a,r,{enumerable:!0,get:e[r]})},o:(t,a)=>Object.prototype.hasOwnProperty.call(t,a)};(()=>{"use strict";function a(t,e){return a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,a){return t.__proto__=a,t},a(t,e)}const e=flarum.core.compat["admin/app"];var r=t.n(e);const n=flarum.core.compat["common/extend"],s=flarum.core.compat["admin/components/DashboardPage"];var i=t.n(s);const o=flarum.core.compat["admin/components/DashboardWidget"];var l=t.n(o);const c=flarum.core.compat["common/components/Alert"];var h=t.n(c),d=function(t){var e,n;function s(){return t.apply(this,arguments)||this}n=t,(e=s).prototype=Object.create(n.prototype),e.prototype.constructor=e,a(e,n);var i=s.prototype;return i.className=function(){return"ReindexWarningWidget"},i.content=function(){return m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle",title:r().translator.trans("blomstra-search.admin.reindex-required.title")},r().translator.trans("blomstra-search.admin.reindex-required.detail"))},s}(l());r().initializers.add("blomstra-search",function(){var t=r().data.settings["blomstra-search.active-index"],a=r().data.settings["blomstra-search.index-compatible"];t&&"v2"!==a&&(0,n.extend)(i().prototype,"availableWidgets",function(t){t.add("blomstra-search-reindex",m(d),110)});var e=new Map;["arabic","armenian","basque","bengali","brazilian","bulgarian","catalan","cjk","czech","danish","dutch","english","estonian","finnish","french","galician","german","greek","hindi","hungarian","indonesian","irish","italian","latvian","lithuanian","norwegian","persian","portuguese","romanian","russian","sorani","spanish","swedish","turkish","thai"].forEach(function(t){e.set(t,t)}),r().extensionData.for("blomstra-search").registerSetting(function(){var a=r().data.settings["blomstra-search.indexed-analyzer"];if(!t||!a)return null;var e=this.setting("blomstra-search.analyzer-language")()||"english",n=this.setting("blomstra-search.min-search-length")()||String(3),s=String(r().data.settings["blomstra-search.indexed-min-search-length"]||3);return e===a&&n===s?null:m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle"},r().translator.trans("blomstra-search.admin.index-settings-changed"))}).registerSetting({setting:"blomstra-search.elastic-endpoint",label:r().translator.trans("blomstra-search.admin.elastic-endpoint"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-username",label:r().translator.trans("blomstra-search.admin.elastic-username"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-password",label:r().translator.trans("blomstra-search.admin.elastic-password"),type:"password"}).registerSetting({setting:"blomstra-search.elastic-index",label:r().translator.trans("blomstra-search.admin.elastic-index"),default:"flarum",type:"input"}).registerSetting({setting:"blomstra-search.analyzer-language",label:r().translator.trans("blomstra-search.admin.analyzer.label"),help:r().translator.trans("blomstra-search.admin.analyzer.help"),type:"select",options:Object.fromEntries(e.entries()),default:"english"}).registerSetting({setting:"blomstra-search.search-discussion-subjects",label:r().translator.trans("blomstra-search.admin.search-discussion-subjects"),type:"switch"}).registerSetting({setting:"blomstra-search.search-post-bodies",label:r().translator.trans("blomstra-search.admin.search-post-bodies"),type:"switch"}).registerSetting({setting:"blomstra-search.min-search-length",label:r().translator.trans("blomstra-search.admin.min-search-length.label"),help:r().translator.trans("blomstra-search.admin.min-search-length.help"),type:"select",options:{1:"1",2:"2",3:"3",4:"4"},default:String(3)})})})(),module.exports={}})(); (()=>{var a={n:t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return a.d(e,{a:e}),e},d:(t,e)=>{for(var n in e)a.o(e,n)&&!a.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},o:(a,t)=>Object.prototype.hasOwnProperty.call(a,t)};(()=>{"use strict";function t(a,e){return t=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(a,t){return a.__proto__=t,a},t(a,e)}const e=flarum.core.compat["admin/app"];var n=a.n(e);const r=flarum.core.compat["common/extend"],s=flarum.core.compat["admin/components/DashboardPage"];var i=a.n(s);const l=flarum.core.compat["admin/components/DashboardWidget"];var o=a.n(l);const c=flarum.core.compat["common/components/Alert"];var h=a.n(c),d=function(a){var e,r;function s(){return a.apply(this,arguments)||this}r=a,(e=s).prototype=Object.create(r.prototype),e.prototype.constructor=e,t(e,r);var i=s.prototype;return i.className=function(){return"ReindexWarningWidget"},i.content=function(){return m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle",title:n().translator.trans("blomstra-search.admin.reindex-required.title")},n().translator.trans("blomstra-search.admin.reindex-required.detail"))},s}(o());n().initializers.add("sbp-search",function(){var a=n().data.settings["blomstra-search.active-index"],t=n().data.settings["blomstra-search.index-compatible"];a&&"v3"!==t&&(0,r.extend)(i().prototype,"availableWidgets",function(a){a.add("blomstra-search-reindex",m(d),110)}),n().extensionData.for("sbp-search").registerSetting(function(){var t=n().data.settings["blomstra-search.indexed-analyzer"];if(!a||!t)return null;var e=this.setting("blomstra-search.analyzer-language")()||"english",r=this.setting("blomstra-search.stem-exclusion")()||"",s=n().data.settings["blomstra-search.indexed-stem-exclusion"]||"";return e===t&&r===s?null:m(h(),{type:"warning",dismissible:!1,icon:"fas fa-exclamation-triangle"},n().translator.trans("blomstra-search.admin.index-settings-changed"))}).registerSetting({setting:"blomstra-search.elastic-endpoint",label:n().translator.trans("blomstra-search.admin.elastic-endpoint"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-username",label:n().translator.trans("blomstra-search.admin.elastic-username"),type:"input"}).registerSetting({setting:"blomstra-search.elastic-password",label:n().translator.trans("blomstra-search.admin.elastic-password"),type:"password"}).registerSetting({setting:"blomstra-search.elastic-index",label:n().translator.trans("blomstra-search.admin.elastic-index"),default:"flarum",type:"input"}).registerSetting({setting:"blomstra-search.search-discussion-subjects",label:n().translator.trans("blomstra-search.admin.search-discussion-subjects"),type:"switch"}).registerSetting({setting:"blomstra-search.search-post-bodies",label:n().translator.trans("blomstra-search.admin.search-post-bodies"),type:"switch"}).registerSetting({setting:"blomstra-search.analyzer-language",label:n().translator.trans("blomstra-search.admin.analyzer.label"),help:n().translator.trans("blomstra-search.admin.analyzer.help"),type:"select",options:{arabic:"Arabic",armenian:"Armenian",basque:"Basque",bengali:"Bengali",brazilian:"Brazilian Portuguese",bulgarian:"Bulgarian",catalan:"Catalan",cjk:"CJK (Chinese, Japanese, Korean)",czech:"Czech",danish:"Danish",dutch:"Dutch",english:"English",estonian:"Estonian",finnish:"Finnish",french:"French",galician:"Galician",german:"German",greek:"Greek",hindi:"Hindi",hungarian:"Hungarian",indonesian:"Indonesian",irish:"Irish",italian:"Italian",latvian:"Latvian",lithuanian:"Lithuanian",norwegian:"Norwegian",persian:"Persian",portuguese:"Portuguese",romanian:"Romanian",russian:"Russian",sorani:"Sorani (Kurdish)",spanish:"Spanish",standard:"Standard (generic)",swedish:"Swedish",turkish:"Turkish",thai:"Thai"},default:"english"}).registerSetting(function(){var a="cjk"===(this.setting("blomstra-search.analyzer-language")()||"english");return this.buildSettingComponent({setting:"blomstra-search.stem-exclusion",type:"textarea",label:n().translator.trans("blomstra-search.admin.settings.stem-exclusion.label"),help:n().translator.trans("blomstra-search.admin.settings.stem-exclusion.help"),disabled:a})}).registerSetting(function(){var a="cjk"===(this.setting("blomstra-search.analyzer-language")()||"english");return this.buildSettingComponent({setting:"blomstra-search.min-search-length",label:n().translator.trans("blomstra-search.admin.min-search-length.label"),help:n().translator.trans("blomstra-search.admin.min-search-length.help"),type:"select",options:{1:"1",2:"2",3:"3",4:"4"},default:n().data.settings["blomstra-search.min-search-length"],disabled:a})})})})(),module.exports={}})();
//# sourceMappingURL=admin.js.map //# sourceMappingURL=admin.js.map

File diff suppressed because one or more lines are too long

2
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

1
js/dist/forum.js.LICENSE.txt vendored Normal file
View File

@ -0,0 +1 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ import DashboardPage from 'flarum/admin/components/DashboardPage';
import DashboardWidget from 'flarum/admin/components/DashboardWidget'; import DashboardWidget from 'flarum/admin/components/DashboardWidget';
import Alert from 'flarum/common/components/Alert'; import Alert from 'flarum/common/components/Alert';
const REQUIRED_INDEX_COMPAT = 'v2'; const REQUIRED_INDEX_COMPAT = 'v3';
class ReindexWarningWidget extends DashboardWidget { class ReindexWarningWidget extends DashboardWidget {
className() { className() {
@ -25,7 +25,7 @@ class ReindexWarningWidget extends DashboardWidget {
} }
} }
app.initializers.add('blomstra-search', () => { app.initializers.add('sbp-search', () => {
const activeIndex = app.data.settings['blomstra-search.active-index']; const activeIndex = app.data.settings['blomstra-search.active-index'];
const compatVersion = app.data.settings['blomstra-search.index-compatible']; const compatVersion = app.data.settings['blomstra-search.index-compatible'];
@ -35,61 +35,57 @@ app.initializers.add('blomstra-search', () => {
}); });
} }
const languages = new Map(); const languages: Record<string, string> = {
[ arabic: 'Arabic',
'arabic', armenian: 'Armenian',
'armenian', basque: 'Basque',
'basque', bengali: 'Bengali',
'bengali', brazilian: 'Brazilian Portuguese',
'brazilian', bulgarian: 'Bulgarian',
'bulgarian', catalan: 'Catalan',
'catalan', cjk: 'CJK (Chinese, Japanese, Korean)',
'cjk', czech: 'Czech',
'czech', danish: 'Danish',
'danish', dutch: 'Dutch',
'dutch', english: 'English',
'english', estonian: 'Estonian',
'estonian', finnish: 'Finnish',
'finnish', french: 'French',
'french', galician: 'Galician',
'galician', german: 'German',
'german', greek: 'Greek',
'greek', hindi: 'Hindi',
'hindi', hungarian: 'Hungarian',
'hungarian', indonesian: 'Indonesian',
'indonesian', irish: 'Irish',
'irish', italian: 'Italian',
'italian', latvian: 'Latvian',
'latvian', lithuanian: 'Lithuanian',
'lithuanian', norwegian: 'Norwegian',
'norwegian', persian: 'Persian',
'persian', portuguese: 'Portuguese',
'portuguese', romanian: 'Romanian',
'romanian', russian: 'Russian',
'russian', sorani: 'Sorani (Kurdish)',
'sorani', spanish: 'Spanish',
'spanish', standard: 'Standard (generic)',
'swedish', swedish: 'Swedish',
'turkish', turkish: 'Turkish',
'thai', thai: 'Thai',
].forEach((language) => { };
languages.set(language, language);
});
app.extensionData app.extensionData
.for('blomstra-search') .for('sbp-search')
.registerSetting(function (this: any) { .registerSetting(function (this: any) {
// No index yet, or first build predates this tracking — stay silent. // No index yet, or first build predates this tracking — stay silent.
const indexedAnalyzer = app.data.settings['blomstra-search.indexed-analyzer']; const indexedAnalyzer = app.data.settings['blomstra-search.indexed-analyzer'];
if (!activeIndex || !indexedAnalyzer) return null; if (!activeIndex || !indexedAnalyzer) return null;
const currentAnalyzer = this.setting('blomstra-search.analyzer-language')() || 'english'; const currentAnalyzer = this.setting('blomstra-search.analyzer-language')() || 'english';
const currentMinLength = this.setting('blomstra-search.min-search-length')(); const currentStemExclusion = this.setting('blomstra-search.stem-exclusion')() || '';
const indexedMinLength = String( const indexedStemExclusion = app.data.settings['blomstra-search.indexed-stem-exclusion'] || '';
app.data.settings['blomstra-search.indexed-min-search-length'] || app.data.settings['blomstra-search.min-search-length']
);
if (currentAnalyzer === indexedAnalyzer && currentMinLength === indexedMinLength) return null; if (currentAnalyzer === indexedAnalyzer && currentStemExclusion === indexedStemExclusion) return null;
return m( return m(
Alert, Alert,
@ -118,14 +114,6 @@ app.initializers.add('blomstra-search', () => {
default: 'flarum', default: 'flarum',
type: 'input', type: 'input',
}) })
.registerSetting({
setting: 'blomstra-search.analyzer-language',
label: app.translator.trans('blomstra-search.admin.analyzer.label'),
help: app.translator.trans('blomstra-search.admin.analyzer.help'),
type: 'select',
options: Object.fromEntries(languages.entries()),
default: 'english',
})
.registerSetting({ .registerSetting({
setting: 'blomstra-search.search-discussion-subjects', setting: 'blomstra-search.search-discussion-subjects',
label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'), label: app.translator.trans('blomstra-search.admin.search-discussion-subjects'),
@ -137,11 +125,33 @@ app.initializers.add('blomstra-search', () => {
type: 'switch', type: 'switch',
}) })
.registerSetting({ .registerSetting({
setting: 'blomstra-search.min-search-length', setting: 'blomstra-search.analyzer-language',
label: app.translator.trans('blomstra-search.admin.min-search-length.label'), label: app.translator.trans('blomstra-search.admin.analyzer.label'),
help: app.translator.trans('blomstra-search.admin.min-search-length.help'), help: app.translator.trans('blomstra-search.admin.analyzer.help'),
type: 'select', type: 'select',
options: { '1': '1', '2': '2', '3': '3', '4': '4' }, options: languages,
default: app.data.settings['blomstra-search.min-search-length'], default: 'english',
})
.registerSetting(function (this: any) {
const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk';
return this.buildSettingComponent({
setting: 'blomstra-search.stem-exclusion',
type: 'textarea',
label: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.label'),
help: app.translator.trans('blomstra-search.admin.settings.stem-exclusion.help'),
disabled: isCjk,
});
})
.registerSetting(function (this: any) {
const isCjk = (this.setting('blomstra-search.analyzer-language')() || 'english') === 'cjk';
return this.buildSettingComponent({
setting: 'blomstra-search.min-search-length',
label: app.translator.trans('blomstra-search.admin.min-search-length.label'),
help: app.translator.trans('blomstra-search.admin.min-search-length.help'),
type: 'select',
options: { '1': '1', '2': '2', '3': '3', '4': '4' },
default: app.data.settings['blomstra-search.min-search-length'],
disabled: isCjk,
});
}); });
}); });

View File

@ -6,10 +6,9 @@ import DiscussionListState from 'flarum/forum/states/DiscussionListState';
export default function extendDiscussionState() { export default function extendDiscussionState() {
override(DiscussionListState.prototype, 'loadPage', async function (this: DiscussionListState, original, page: number = 1) { override(DiscussionListState.prototype, 'loadPage', async function (this: DiscussionListState, original, page: number = 1) {
const preloaded = app.data.apiDocument || null;
// If existing payload is given or no search is made, fallback on native page. // If existing payload is given or no search is made, fallback on native page.
if (preloaded || !this.requestParams()?.filter?.q) return original.call(this, page); if (!this.requestParams()?.filter?.q) return original.call(this, page);
const params = this.requestParams(); const params = this.requestParams();
params.page = { params.page = {
@ -17,18 +16,13 @@ export default function extendDiscussionState() {
...params.page, ...params.page,
}; };
// Default to latest sort when searching without an explicit sort parameter.
// Matches the backend default and keeps the dropdown label in sync.
if (!params.sort) {
params.sort = '-lastPostedAt';
}
if (Array.isArray(params.include)) { if (Array.isArray(params.include)) {
params.include = params.include.join(','); params.include = params.include.join(',');
} }
// Construct API search URI // Construct API search URI
const url = `${app.forum.attribute('apiUrl')}/blomstra/search/${this.type}`; const url = `${app.forum.attribute('apiUrl')}/sbp/search/${this.type}`;
// Make API GET request // Make API GET request
const results = await app.request({ params, url, method: 'GET' }); const results = await app.request({ params, url, method: 'GET' });

View File

@ -21,18 +21,21 @@ export default class DiscussionsSearchSource implements SearchSource {
private type = 'discussions'; private type = 'discussions';
async search(query: string): Promise<void> { async search(query: string): Promise<void> {
// Suppress ES instant results when "search within comments" is ON;
// only usernames are shown in that mode.
query = query.toLowerCase(); query = query.toLowerCase();
this.results.set(query, []); this.results.set(query, []);
const params = { const params = {
filter: { q: query }, filter: { q: query, autocomplete: 1 },
page: { limit: 3 }, page: { limit: 3 },
include: 'mostRelevantPost', include: 'mostRelevantPost',
}; };
// Construct API search URI // Construct API search URI
const url = `${app.forum.attribute('apiUrl')}/blomstra/search/${this.type}`; const url = `${app.forum.attribute('apiUrl')}/sbp/search/${this.type}`;
// Make API GET request // Make API GET request
const results = await app.request({ params, url, method: 'GET' }); const results = await app.request({ params, url, method: 'GET' });
@ -66,7 +69,7 @@ export default class DiscussionsSearchSource implements SearchSource {
return [ return [
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>, <li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
<li> <li>
<LinkButton icon="fas fa-search" href={app.route('index', { q: query, sort: 'latest' })}> <LinkButton icon="fas fa-search" href={app.route('index', { q: query })}>
{app.translator.trans('core.forum.search.all_discussions_button', { query })} {app.translator.trans('core.forum.search.all_discussions_button', { query })}
</LinkButton> </LinkButton>
</li>, </li>,

View File

@ -0,0 +1,14 @@
diff --git a/src/Commands/BuildCommand.php b/src/Commands/BuildCommand.php
index 9024e1e..9024e1e 100644
--- a/src/Commands/BuildCommand.php
+++ b/src/Commands/BuildCommand.php
@@ -38,6 +38,6 @@ class BuildCommand extends Command
/** Matches Flarum's Search::MIN_SEARCH_LEN — the default minimum query length. */
public const DEFAULT_MIN_SEARCH_LENGTH = 3;
/** N-gram bounds for the title autocomplete subfield (not admin-configurable). */
- private const TITLE_MIN_GRAM = 2;
- private const TITLE_MAX_GRAM = 15;
+ private const TITLE_MIN_GRAM = 3;
+ private const TITLE_MAX_GRAM = 20;
protected $signature = 'blomstra:search:index

View File

@ -17,9 +17,12 @@ blomstra-search:
search-post-bodies: Search inside comments search-post-bodies: Search inside comments
match-sentences: Match search term against full sentence match-sentences: Match search term against full sentence
match-words: Match search term against full words match-words: Match search term against full words
settings:
stem-exclusion:
label: "Protected words (stem exclusion)"
help: "One word per line. These words will not be stemmed during search (e.g. brand names, proper nouns). Not applicable for CJK or Thai languages. Requires a full re-index to take effect."
min-search-length: min-search-length:
label: Minimum search query length label: Minimum search query length
help: | help: |
Minimum number of characters required before a search is triggered. Lower this to 1 or 2 Minimum number of characters required before a search is triggered. Lower this to 1 or 2
for CJK (Chinese, Japanese, Korean) communities where single characters carry full meaning. for CJK (Chinese, Japanese, Korean) communities where single characters carry full meaning.
Changing this requires a full index rebuild to take effect.

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Api; namespace Sbp\Search\Api;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Api\Controllers; namespace Sbp\Search\Api\Controllers;
use Blomstra\Search\Commands\BuildCommand; use Sbp\Search\Commands\BuildCommand;
use Laminas\Diactoros\Response\EmptyResponse; use Laminas\Diactoros\Response\EmptyResponse;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@ -10,15 +10,15 @@
* *
*/ */
namespace Blomstra\Search\Api\Controllers; namespace Sbp\Search\Api\Controllers;
use Blomstra\Search\Elasticsearch\HasChildQuery; use Sbp\Search\Elasticsearch\HasChildQuery;
use Blomstra\Search\Elasticsearch\MatchPhraseQuery; use Sbp\Search\Elasticsearch\MatchPhraseQuery;
use Blomstra\Search\Elasticsearch\MatchQuery; use Sbp\Search\Elasticsearch\MatchQuery;
use Blomstra\Search\Elasticsearch\TermsQuery; use Sbp\Search\Elasticsearch\TermsQuery;
use Blomstra\Search\Searchers\CommentPostSearcher; use Sbp\Search\Searchers\CommentPostSearcher;
use Blomstra\Search\Searchers\DiscussionSearcher; use Sbp\Search\Searchers\DiscussionSearcher;
use Blomstra\Search\Searchers\Searcher; use Sbp\Search\Searchers\Searcher;
use Elasticsearch\Client; use Elasticsearch\Client;
use Flarum\Api\Controller\ListDiscussionsController; use Flarum\Api\Controller\ListDiscussionsController;
use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Api\Serializer\DiscussionSerializer;
@ -37,7 +37,7 @@ use Illuminate\Support\Str;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Spatie\ElasticsearchQueryBuilder\Builder; use Spatie\ElasticsearchQueryBuilder\Builder;
use Blomstra\Search\Elasticsearch\BoolQuery; use Sbp\Search\Elasticsearch\BoolQuery;
use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery; use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery;
use Spatie\ElasticsearchQueryBuilder\Sorts\Sort; use Spatie\ElasticsearchQueryBuilder\Sorts\Sort;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
@ -78,6 +78,11 @@ class SearchController extends ListDiscussionsController
$offset = $this->extractOffset($request); $offset = $this->extractOffset($request);
$include = array_merge($this->extractInclude($request), ['state']); $include = array_merge($this->extractInclude($request), ['state']);
// Autocomplete path: fast prefix match on title.autocomplete only.
if (!empty($filters['autocomplete']) && !empty($search)) {
return $this->handleAutocomplete($search, $actor, $limit, $offset, $include, $document, $request);
}
$knownSortFields = array_merge(array_values($this->translateSort), ['rawId']); $knownSortFields = array_merge(array_values($this->translateSort), ['rawId']);
$logger = resolve(LoggerInterface::class); $logger = resolve(LoggerInterface::class);
@ -171,7 +176,7 @@ class SearchController extends ListDiscussionsController
}); });
$document->addPaginationLinks( $document->addPaginationLinks(
$this->uri->to('api')->route('blomstra.search', ['type' => 'discussions']), $this->uri->to('api')->route('sbp.search', ['type' => 'discussions']),
$request->getQueryParams(), $request->getQueryParams(),
$offset, $offset,
$limit, $limit,
@ -217,6 +222,104 @@ class SearchController extends ListDiscussionsController
return $discussions; return $discussions;
} }
/**
* Fast autocomplete path: match prefix n-grams on title.autocomplete only.
* No has_child, no phrase scoring a single MatchQuery suffices.
*/
protected function handleAutocomplete(
string $search,
User $actor,
int $limit,
int $offset,
array $include,
Document $document,
ServerRequestInterface $request
): mixed {
$query = BoolQuery::create()
->add(TermQuery::create('join_field', 'discussion'), 'filter');
$query->add($this->buildAutocompleteQuery($search), 'must');
$this->addFilters($query, $actor, []);
$payload = (new Builder($this->elastic))
->index(resolve('blomstra.search.elastic_index'))
->addQuery($query)
->addSort(new Sort('updated_at', 'desc'))
->getPayload();
$response = $this->elastic->search([
'index' => resolve('blomstra.search.elastic_index'),
'size' => $limit + 1,
'from' => $offset,
'body' => $payload,
]);
Discussion::setStateUser($actor);
$results = Collection::make(Arr::get($response, 'hits.hits'))
->map(fn ($hit) => [
'discussion_id' => Str::after($hit['_id'], 'discussions:'),
'most_relevant_post_id' => null,
'weight' => Arr::get($hit, 'sort.0', 0),
]);
$document->addPaginationLinks(
$this->uri->to('api')->route('sbp.search', ['type' => 'discussions']),
$request->getQueryParams(),
$offset,
$limit,
$results->count() > $limit ? null : 0
);
$results = $results->take($limit);
$discussions = Discussion::query()
->when(
$actor->isGuest() || !$actor->hasPermission('discussion.hide'),
fn ($q) => $q->whereNull('hidden_at')
)
->whereIn('id', $results->pluck('discussion_id')->filter())
->get()
->each(function (Discussion $discussion) use ($results) {
$result = $results->firstWhere('discussion_id', $discussion->id);
$discussion->most_relevant_post_id = $result['most_relevant_post_id'] ?? $discussion->first_post_id;
$discussion->weight = $result['weight'] ?? 0;
})
->sortByDesc('updated_at')
->unique();
$this->loadRelations($discussions, $include);
return $discussions;
}
protected function buildAutocompleteQuery(string $search): BoolQuery
{
$query = BoolQuery::create();
// Title prefix match (stronger)
$query->add(
(new MatchQuery('title.autocomplete', $search))
->operator('and')
->boost(1.0),
'should'
);
// Content prefix match (weaker)
$query->add(
(new MatchQuery('content.autocomplete', $search))
->operator('and')
->boost(0.4),
'should'
);
$query->minimumShouldMatch(1);
return $query;
}
/** /**
* Build the text-matching portion of the query. * Build the text-matching portion of the query.
* *
@ -233,6 +336,31 @@ class SearchController extends ListDiscussionsController
protected function buildTextQuery(string $search, User $actor, bool $needsScoring = false): BoolQuery protected function buildTextQuery(string $search, User $actor, bool $needsScoring = false): BoolQuery
{ {
$textQuery = BoolQuery::create(); $textQuery = BoolQuery::create();
// Strong direct title match for the full search page.
$textQuery->add(
(new MatchQuery('title', $search))
->operator('and')
->boost(4.0),
'should'
);
// Low-weight prefix match on title.autocomplete for partial title hits.
$textQuery->add(
(new MatchQuery('title.autocomplete', $search))
->operator('and')
->boost(0.35),
'should'
);
// Low-weight prefix match on title.autocomplete for the full search page.
// Exact / normal title matches should still rank higher than prefix-only hits.
$textQuery->add(
(new MatchQuery('title.autocomplete', $search))
->operator('and')
->boost(0.35),
'should'
);
if ($this->discussionSearcher?->enabled()) { if ($this->discussionSearcher?->enabled()) {
$textQuery->add($this->buildShouldClauses($search, $this->discussionSearcher->boost()), 'should'); $textQuery->add($this->buildShouldClauses($search, $this->discussionSearcher->boost()), 'should');
@ -266,9 +394,10 @@ class SearchController extends ListDiscussionsController
if ($this->matchSentences) { if ($this->matchSentences) {
$should->add((new MatchPhraseQuery('content', $search))->boost(2 * $boost), 'should'); $should->add((new MatchPhraseQuery('content', $search))->boost(2 * $boost), 'should');
} }
if ($this->matchWords) { if ($this->matchWords) {
$should->add((new MatchQuery('content', $search))->operator('and')->boost(1.8 * $boost), 'should'); $should->add((new MatchQuery('content', $search))->operator('and')->boost(1.8 * $boost), 'should');
} $should->add((new MatchQuery('content.autocomplete', $search))->operator('and')->boost(0.30 * $boost), 'should');
}
return $should; return $should;
} }

View File

@ -10,11 +10,11 @@
* *
*/ */
namespace Blomstra\Search\Commands; namespace Sbp\Search\Commands;
use Blomstra\Search\Jobs\Job; use Sbp\Search\Jobs\Job;
use Blomstra\Search\Jobs\UpdateSearchJob; use Sbp\Search\Jobs\UpdateSearchJob;
use Blomstra\Search\Seeders\Seeder; use Sbp\Search\Seeders\Seeder;
use Elasticsearch\Client; use Elasticsearch\Client;
use Elasticsearch\Common\Exceptions\ElasticsearchException; use Elasticsearch\Common\Exceptions\ElasticsearchException;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
@ -36,11 +36,15 @@ class BuildCommand extends Command
* read back into blomstra-search.index-compatible by saveIndexedConfig so the * read back into blomstra-search.index-compatible by saveIndexedConfig so the
* value always reflects the live index including after a promote/rollback. * value always reflects the live index including after a promote/rollback.
*/ */
public const INDEX_COMPAT_VERSION = 'v2'; public const INDEX_COMPAT_VERSION = 'v3';
/** Matches Flarum's Search::MIN_SEARCH_LEN — the default minimum query length. */ /** Matches Flarum's Search::MIN_SEARCH_LEN — the default minimum query length. */
public const DEFAULT_MIN_SEARCH_LENGTH = 3; public const DEFAULT_MIN_SEARCH_LENGTH = 3;
/** N-gram bounds for the title autocomplete subfield (not admin-configurable). */
private const TITLE_MIN_GRAM = 3;
private const TITLE_MAX_GRAM = 20;
protected $signature = 'blomstra:search:index protected $signature = 'blomstra:search:index
{action? : build | promote | rollback | discard | mapping | fill} {action? : build | promote | rollback | discard | mapping | fill}
{--fresh} {--fresh}
@ -491,77 +495,128 @@ HELP;
$settingsResponse = $client->indices()->getSettings(['index' => $indexName]); $settingsResponse = $client->indices()->getSettings(['index' => $indexName]);
$analysis = Arr::get($settingsResponse, "$indexName.settings.index.analysis", []); $analysis = Arr::get($settingsResponse, "$indexName.settings.index.analysis", []);
$analyzer = Arr::get($analysis, 'analyzer.flarum_analyzer.type', 'english'); $analyzer = Arr::get($analysis, 'analyzer.flarum_analyzer.type', 'english');
$minGram = (int) Arr::get($analysis, 'filter.partial_search_filter.min_gram', self::DEFAULT_MIN_SEARCH_LENGTH); $stemExclusion = Arr::get($analysis, 'analyzer.flarum_analyzer.stem_exclusion', []);
$mappingResponse = $client->indices()->getMapping(['index' => $indexName]); $mappingResponse = $client->indices()->getMapping(['index' => $indexName]);
$compatVersion = Arr::get($mappingResponse, "$indexName.mappings._meta.index_compat_version"); $compatVersion = Arr::get($mappingResponse, "$indexName.mappings._meta.index_compat_version");
$settings->set('blomstra-search.indexed-analyzer', $analyzer); $settings->set('blomstra-search.indexed-analyzer', $analyzer);
$settings->set('blomstra-search.indexed-min-search-length', $minGram); $settings->set('blomstra-search.indexed-stem-exclusion', implode("\n", $stemExclusion));
$settings->set('blomstra-search.index-compatible', $compatVersion); $settings->set('blomstra-search.index-compatible', $compatVersion);
} }
protected function buildIndexSettings(SettingsRepositoryInterface $settings): array protected function buildIndexSettings(SettingsRepositoryInterface $settings): array
{ {
$language = $settings->get('blomstra-search.analyzer-language') ?: 'english'; $language = $settings->get('blomstra-search.analyzer-language') ?: 'english';
$minGram = max(1, (int) ($settings->get('blomstra-search.min-search-length') ?: self::DEFAULT_MIN_SEARCH_LENGTH));
$maxGram = 10;
if ($minGram >= $maxGram) { $raw = $settings->get('blomstra-search.stem-exclusion', '');
$this->error("min_gram ($minGram) must be less than max_gram ($maxGram). Using default."); $stemExclusion = array_values(array_filter(array_map('trim', explode("\n", $raw))));
$minGram = self::DEFAULT_MIN_SEARCH_LENGTH;
if ($language === 'cjk') {
// CJK uses the built-in bigram analyzer; no autocomplete subfield.
return [
'analysis' => [
'analyzer' => [
'flarum_analyzer' => ['type' => 'cjk'],
],
],
];
}
$analyzerConfig = ['type' => $language];
if (!empty($stemExclusion)) {
$analyzerConfig['stem_exclusion'] = $stemExclusion;
} }
return [ return [
'index.max_ngram_diff' => $maxGram - $minGram, 'index.max_ngram_diff' => self::TITLE_MAX_GRAM - self::TITLE_MIN_GRAM,
'analysis' => [ 'analysis' => [
'analyzer' => [ 'filter' => [
'flarum_analyzer' => [ 'title_autocomplete_filter' => [
'type' => $language, 'type' => 'edge_ngram',
'min_gram' => self::TITLE_MIN_GRAM,
'max_gram' => self::TITLE_MAX_GRAM,
'token_chars' => ['letter', 'digit'],
], ],
'flarum_analyzer_partial' => [ ],
'analyzer' => [
// Symmetric language analyzer: used for BOTH index and search on content + title.
'flarum_analyzer' => $analyzerConfig,
// Title autocomplete index-time: prefix n-grams, no stemming.
'flarum_title_autocomplete' => [
'type' => 'custom', 'type' => 'custom',
'tokenizer' => 'standard', 'tokenizer' => 'standard',
'filter' => ['lowercase', 'partial_search_filter'], 'filter' => ['lowercase', 'title_autocomplete_filter'],
], ],
], // Title autocomplete search-time: just lowercase — predictable on partial words.
'filter' => [ 'flarum_title_search' => [
'partial_search_filter' => [ 'type' => 'custom',
'type' => 'ngram', 'tokenizer' => 'standard',
'min_gram' => $minGram, 'filter' => ['lowercase'],
'max_gram' => $maxGram,
'token_chars' => ['letter', 'digit', 'symbol'],
], ],
], ],
], ],
]; ];
} }
protected function mappingProperties(): array protected function mappingProperties(): array
{ {
return [ $language = resolve(SettingsRepositoryInterface::class)->get('blomstra-search.analyzer-language') ?: 'english';
'_meta' => ['index_compat_version' => self::INDEX_COMPAT_VERSION],
'properties' => [ $titleMapping = [
'join_field' => ['type' => 'join', 'relations' => ['discussion' => 'post']], 'type' => 'text',
'discussion_id' => ['type' => 'integer'], 'analyzer' => 'flarum_analyzer',
'content' => ['type' => 'text', 'analyzer' => 'flarum_analyzer_partial', 'search_analyzer' => 'flarum_analyzer'], ];
'rawId' => ['type' => 'integer'],
'created_at' => ['type' => 'date'], // Only add the autocomplete subfield for non-CJK languages.
'updated_at' => ['type' => 'date'], if ($language !== 'cjk') {
'is_private' => ['type' => 'boolean'], $titleMapping['fields'] = [
'is_sticky' => ['type' => 'boolean'], 'autocomplete' => [
'groups' => ['type' => 'integer'], 'type' => 'text',
'tags' => ['type' => 'integer'], 'analyzer' => 'flarum_title_autocomplete',
'recipient_groups' => ['type' => 'integer'], 'search_analyzer' => 'flarum_title_search',
'recipient_users' => ['type' => 'integer'],
'comment_count' => ['type' => 'integer'],
'view_count' => ['type' => 'integer'],
'is_hidden' => ['type' => 'boolean'],
], ],
]; ];
} }
$contentMapping = [
'type' => 'text',
'analyzer' => 'flarum_analyzer',
];
// Only add the autocomplete subfield for non-CJK languages.
if ($language !== 'cjk') {
$contentMapping['fields'] = [
'autocomplete' => [
'type' => 'text',
'analyzer' => 'flarum_title_autocomplete',
'search_analyzer' => 'flarum_title_search',
],
];
}
return [
'_meta' => ['index_compat_version' => self::INDEX_COMPAT_VERSION],
'properties' => [
'join_field' => ['type' => 'join', 'relations' => ['discussion' => 'post']],
'discussion_id' => ['type' => 'integer'],
'content' => $contentMapping,
'title' => $titleMapping,
'rawId' => ['type' => 'integer'],
'created_at' => ['type' => 'date'],
'updated_at' => ['type' => 'date'],
'is_private' => ['type' => 'boolean'],
'user_id' => ['type' => 'integer'],
'groups' => ['type' => 'keyword'],
'comment_count' => ['type' => 'integer'],
'tags' => ['type' => 'keyword'],
'is_sticky' => ['type' => 'boolean'],
'is_hidden' => ['type' => 'boolean'],
],
];
}
/** /**
* Query ES for rawIds already indexed in $targetIndex for the given $joinRelation and ID range. * Query ES for rawIds already indexed in $targetIndex for the given $joinRelation and ID range.
* Retries on transient ES failures (NoNodesAvailableException / other ElasticsearchException) * Retries on transient ES failures (NoNodesAvailableException / other ElasticsearchException)

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Elasticsearch; namespace Sbp\Search\Elasticsearch;
class BoolQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\BoolQuery class BoolQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\BoolQuery
{ {

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Elasticsearch; namespace Sbp\Search\Elasticsearch;
use Spatie\ElasticsearchQueryBuilder\Queries\Query; use Spatie\ElasticsearchQueryBuilder\Queries\Query;

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Elasticsearch; namespace Sbp\Search\Elasticsearch;
use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery; use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery;

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Elasticsearch; namespace Sbp\Search\Elasticsearch;
class MatchQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery class MatchQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery
{ {
@ -62,6 +62,10 @@ class MatchQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery
$query['match'][$this->field]['boost'] = $this->boost; $query['match'][$this->field]['boost'] = $this->boost;
$query['match'][$this->field]['zero_terms_query'] = $this->zeroTerms ? 'all' : 'none'; $query['match'][$this->field]['zero_terms_query'] = $this->zeroTerms ? 'all' : 'none';
if ($this->analyzer !== null) {
$query['match'][$this->field]['analyzer'] = $this->analyzer;
}
return $query; return $query;
} }
} }

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Elasticsearch; namespace Sbp\Search\Elasticsearch;
use Spatie\ElasticsearchQueryBuilder\Queries\Query; use Spatie\ElasticsearchQueryBuilder\Queries\Query;

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Elasticsearch; namespace Sbp\Search\Elasticsearch;
use Spatie\ElasticsearchQueryBuilder\Queries\Query; use Spatie\ElasticsearchQueryBuilder\Queries\Query;

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Elasticsearch; namespace Sbp\Search\Elasticsearch;
class WildcardQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\WildcardQuery class WildcardQuery extends \Spatie\ElasticsearchQueryBuilder\Queries\WildcardQuery
{ {

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Exceptions; namespace Sbp\Search\Exceptions;
use Throwable; use Throwable;

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Jobs; namespace Sbp\Search\Jobs;
use Elasticsearch\Client; use Elasticsearch\Client;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Jobs; namespace Sbp\Search\Jobs;
use Blomstra\Search\Seeders\Seeder; use Sbp\Search\Seeders\Seeder;
use Flarum\Queue\AbstractJob; use Flarum\Queue\AbstractJob;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Jobs; namespace Sbp\Search\Jobs;
use Blomstra\Search\Exceptions\SeedingException; use Sbp\Search\Exceptions\SeedingException;
use Elasticsearch\Client; use Elasticsearch\Client;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Jobs; namespace Sbp\Search\Jobs;
use Elasticsearch\Client; use Elasticsearch\Client;
use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Api\Serializer\DiscussionSerializer;

View File

@ -10,12 +10,12 @@
* *
*/ */
namespace Blomstra\Search; namespace Sbp\Search;
use Blomstra\Search\Jobs\DeletingJob; use Sbp\Search\Jobs\DeletingJob;
use Blomstra\Search\Jobs\Job; use Sbp\Search\Jobs\Job;
use Blomstra\Search\Jobs\UpdateSearchJob; use Sbp\Search\Jobs\UpdateSearchJob;
use Blomstra\Search\Jobs\ViewsSearchJob; use Sbp\Search\Jobs\ViewsSearchJob;
use Elasticsearch\Client as Elastic; use Elasticsearch\Client as Elastic;
use Elasticsearch\ClientBuilder; use Elasticsearch\ClientBuilder;
use Flarum\Api\Client; use Flarum\Api\Client;

View File

@ -10,7 +10,7 @@
* *
*/ */
namespace Blomstra\Search\Save; namespace Sbp\Search\Save;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Searchers; namespace Sbp\Search\Searchers;
use Blomstra\Search\Seeders\CommentSeeder; use Sbp\Search\Seeders\CommentSeeder;
class CommentPostSearcher extends Searcher class CommentPostSearcher extends Searcher
{ {

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Searchers; namespace Sbp\Search\Searchers;
use Blomstra\Search\Seeders\DiscussionSeeder; use Sbp\Search\Seeders\DiscussionSeeder;
class DiscussionSearcher extends Searcher class DiscussionSearcher extends Searcher
{ {

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Searchers; namespace Sbp\Search\Searchers;
use Blomstra\Search\Seeders\Seeder; use Sbp\Search\Seeders\Seeder;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
abstract class Searcher abstract class Searcher

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Seeders; namespace Sbp\Search\Seeders;
use Blomstra\Search\Save\Document; use Sbp\Search\Save\Document;
use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Api\Serializer\PostSerializer; use Flarum\Api\Serializer\PostSerializer;
use Flarum\Discussion\Discussion; use Flarum\Discussion\Discussion;

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Seeders; namespace Sbp\Search\Seeders;
use Blomstra\Search\Save\Document; use Sbp\Search\Save\Document;
use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion; use Flarum\Discussion\Discussion;
use Flarum\Discussion\Event as Core; use Flarum\Discussion\Event as Core;
@ -121,6 +121,7 @@ class DiscussionSeeder extends Seeder
'id' => $this->type().':'.$model->id, 'id' => $this->type().':'.$model->id,
'rawId' => $model->id, 'rawId' => $model->id,
'content' => $model->title, 'content' => $model->title,
'title' => $model->title,
'created_at' => $model->created_at?->toAtomString(), 'created_at' => $model->created_at?->toAtomString(),
'updated_at' => $model->last_posted_at?->toAtomString(), 'updated_at' => $model->last_posted_at?->toAtomString(),
'is_private' => $model->is_private, 'is_private' => $model->is_private,

View File

@ -10,9 +10,9 @@
* *
*/ */
namespace Blomstra\Search\Seeders; namespace Sbp\Search\Seeders;
use Blomstra\Search\Save\Document; use Sbp\Search\Save\Document;
use Flarum\Extension\ExtensionManager; use Flarum\Extension\ExtensionManager;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;