Compare commits

...

31 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
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
35 changed files with 200 additions and 123 deletions

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),

2
js/dist/admin.js vendored
View File

@ -1,2 +1,2 @@
(()=>{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("blomstra-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("blomstra-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",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={}})(); (()=>{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

@ -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'];
@ -75,7 +75,7 @@ app.initializers.add('blomstra-search', () => {
}; };
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'];

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

@ -23,9 +23,6 @@ export default class DiscussionsSearchSource implements SearchSource {
async search(query: string): Promise<void> { async search(query: string): Promise<void> {
// Suppress ES instant results when "search within comments" is ON; // Suppress ES instant results when "search within comments" is ON;
// only usernames are shown in that mode. // only usernames are shown in that mode.
if (app.forum.attribute('blomstraSearchPostBodies')) {
return;
}
query = query.toLowerCase(); query = query.toLowerCase();
@ -38,7 +35,7 @@ export default class DiscussionsSearchSource implements SearchSource {
}; };
// 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' });
@ -72,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

@ -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;
@ -176,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,
@ -265,7 +265,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,
@ -294,11 +294,30 @@ class SearchController extends ListDiscussionsController
return $discussions; return $discussions;
} }
protected function buildAutocompleteQuery(string $search): MatchQuery
protected function buildAutocompleteQuery(string $search): BoolQuery
{ {
return (new MatchQuery('title.autocomplete', $search)) $query = BoolQuery::create();
// Title prefix match (stronger)
$query->add(
(new MatchQuery('title.autocomplete', $search))
->operator('and') ->operator('and')
->boost(1.0); ->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;
} }
/** /**
@ -317,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');
@ -352,6 +396,7 @@ class SearchController extends ListDiscussionsController
} }
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;
@ -581,29 +581,42 @@ HELP;
]; ];
} }
$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 [ return [
'_meta' => ['index_compat_version' => self::INDEX_COMPAT_VERSION], '_meta' => ['index_compat_version' => self::INDEX_COMPAT_VERSION],
'properties' => [ 'properties' => [
'join_field' => ['type' => 'join', 'relations' => ['discussion' => 'post']], 'join_field' => ['type' => 'join', 'relations' => ['discussion' => 'post']],
'discussion_id' => ['type' => 'integer'], 'discussion_id' => ['type' => 'integer'],
'content' => ['type' => 'text', 'analyzer' => 'flarum_analyzer'], 'content' => $contentMapping,
'title' => $titleMapping, 'title' => $titleMapping,
'rawId' => ['type' => 'integer'], 'rawId' => ['type' => 'integer'],
'created_at' => ['type' => 'date'], 'created_at' => ['type' => 'date'],
'updated_at' => ['type' => 'date'], 'updated_at' => ['type' => 'date'],
'is_private' => ['type' => 'boolean'], 'is_private' => ['type' => 'boolean'],
'is_sticky' => ['type' => 'boolean'], 'user_id' => ['type' => 'integer'],
'groups' => ['type' => 'integer'], 'groups' => ['type' => 'keyword'],
'tags' => ['type' => 'integer'],
'recipient_groups' => ['type' => 'integer'],
'recipient_users' => ['type' => 'integer'],
'comment_count' => ['type' => 'integer'], 'comment_count' => ['type' => 'integer'],
'view_count' => ['type' => 'integer'], 'tags' => ['type' => 'keyword'],
'is_sticky' => ['type' => 'boolean'],
'is_hidden' => ['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
{ {

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;

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;