Skip to content

Commit 75c969f

Browse files
Meilisearch Implementation (statamic#1539)
Co-authored-by: Jack McDade <[email protected]>
1 parent 9db23e0 commit 75c969f

28 files changed

+484
-90
lines changed

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16.20

app/Providers/AppServiceProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function boot()
2727
return [new DescriptionListExtension, new HintExtension, new TabbedCodeBlockExtension, new AttributesExtension];
2828
});
2929

30-
if (config('torchlight.token')) {
30+
if (config('torchlight.token') && ! app()->runningConsoleCommand('search:update')) {
3131
Markdown::addExtensions(function () {
3232
return [new TorchlightExtension];
3333
});

app/Providers/EventServiceProvider.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
namespace App\Providers;
44

5-
use Illuminate\Support\Facades\Event;
5+
use App\Search\Listeners\SearchEntriesCreatedListener;
66
use Illuminate\Auth\Events\Registered;
77
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
88
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
9+
use Stillat\DocumentationSearch\Events\SearchEntriesCreated;
910

1011
class EventServiceProvider extends ServiceProvider
1112
{
@@ -18,6 +19,9 @@ class EventServiceProvider extends ServiceProvider
1819
Registered::class => [
1920
SendEmailVerificationNotification::class,
2021
],
22+
SearchEntriesCreated::class => [
23+
SearchEntriesCreatedListener::class,
24+
],
2125
];
2226

2327
/**

app/Search/DocTransformer.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace App\Search;
4+
5+
use Illuminate\Support\Str;
6+
use Stillat\DocumentationSearch\Contracts\DocumentTransformer;
7+
use Stillat\DocumentationSearch\Document\DocumentFragment;
8+
9+
class DocTransformer implements DocumentTransformer
10+
{
11+
public function handle(DocumentFragment $fragment, $entry): void
12+
{
13+
// Add some extra details to "additional_context"
14+
if (Str::containsAll($fragment->content, ['clear', 'cache'])) {
15+
$fragment->additionalContextData[] = 'delete cache';
16+
}
17+
18+
if (Str::contains($fragment->content, 'JS Drivers')) {
19+
$fragment->additionalContextData[] = 'javascript drivers';
20+
}
21+
}
22+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace App\Search\Listeners;
4+
5+
use Stillat\DocumentationSearch\Events\SearchEntriesCreated;
6+
7+
class SearchEntriesCreatedListener
8+
{
9+
protected function getParentHeadings($headers, $target)
10+
{
11+
$hierarchy = [];
12+
$levels = [];
13+
14+
$currentLevel = $target->level;
15+
16+
for ($i = array_search($target, $headers) - 1; $i >= 0; $i--) {
17+
$header = $headers[$i];
18+
19+
if ($header->level < $currentLevel) {
20+
$hierarchy[] = $header;
21+
$currentLevel = $header->level;
22+
}
23+
}
24+
25+
foreach (array_reverse($hierarchy) as $level) {
26+
$levels[$level->level] = $level->text;
27+
}
28+
29+
return $levels;
30+
}
31+
32+
/**
33+
* Handle the event.
34+
*/
35+
public function handle(SearchEntriesCreated $event): void
36+
{
37+
$collection = $event->entry->collection()->title;
38+
39+
$headers = [];
40+
41+
foreach ($event->sections as $section) {
42+
if ($section->fragment->headerDetails == null) {
43+
continue;
44+
}
45+
46+
$headers[] = $section->fragment->headerDetails;
47+
}
48+
49+
foreach ($event->sections as $section) {
50+
$data = $section->searchEntry->data();
51+
$category = $collection.' » '.$data['origin_title'];
52+
53+
$parentHeadings = null;
54+
55+
if (
56+
$section->fragment->headerDetails != null &&
57+
$section->fragment->headerDetails->level >= 3
58+
) {
59+
$header = $section->fragment->headerDetails;
60+
61+
$parentHeadings = $this->getParentHeadings(
62+
$headers,
63+
$header
64+
);
65+
$parentHeadings[$header->level] = $header->text;
66+
}
67+
68+
if ($parentHeadings === null) {
69+
$parentHeadings = [];
70+
71+
if ($data['search_title'] != null && $data['origin_title'] != $data['search_title']) {
72+
$parentHeadings[1] = $data['search_title'];
73+
}
74+
}
75+
76+
if (count($parentHeadings) > 2) {
77+
array_shift($parentHeadings);
78+
}
79+
80+
$data['hierarchy_lvl0'] = $category;
81+
$data['hierarchy_lvl1'] = implode(' » ', $parentHeadings);
82+
83+
if ($data['is_root']) {
84+
$data['content'] = $event->entry->intro ?? $event->entry->description ?? $data['search_content'];
85+
} else {
86+
$data['content'] = $data['search_content'] ?? '';
87+
}
88+
89+
$data['url'] = $data['search_url'];
90+
91+
$section->searchEntry->data($data);
92+
}
93+
}
94+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Search;
4+
5+
use DOMDocument;
6+
use Illuminate\Http\Request;
7+
use Statamic\Contracts\Entries\Entry;
8+
use Statamic\Facades\Cascade;
9+
use Stillat\DocumentationSearch\Contracts\ContentRetriever;
10+
11+
class RequestContentRetriever implements ContentRetriever
12+
{
13+
public function getContent(Entry $entry): string
14+
{
15+
$originalRequest = app('request');
16+
$request = tap(Request::capture(), function ($request) {
17+
app()->instance('request', $request);
18+
Cascade::withRequest($request);
19+
});
20+
21+
$content = '';
22+
23+
try {
24+
$content = $entry->toResponse($request)->getContent();
25+
} finally {
26+
app()->instance('request', $originalRequest);
27+
}
28+
29+
return $this->extractArticleContent($content);
30+
}
31+
32+
protected function extractArticleContent(string $content): string
33+
{
34+
$dom = new DOMDocument;
35+
36+
libxml_use_internal_errors(true);
37+
$dom->loadHTML($content);
38+
libxml_clear_errors();
39+
40+
$articles = $dom->getElementsByTagName('article');
41+
42+
$result = '';
43+
44+
foreach ($articles as $article) {
45+
$result .= $dom->saveHTML($article);
46+
}
47+
48+
return $result;
49+
}
50+
}

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
"laravel/framework": "^10.0",
1313
"laravel/tinker": "^2.0",
1414
"league/commonmark": "^2.0",
15+
"statamic-rad-pack/meilisearch": "^3.2",
1516
"statamic/cms": "^4.0",
1617
"statamic/ssg": "^2.0",
17-
"torchlight/torchlight-commonmark": "^0.5.5"
18+
"torchlight/torchlight-commonmark": "^0.5.5",
19+
"stillat/documentation-search": "^1.2.0"
1820
},
1921
"require-dev": {
2022
"beyondcode/laravel-dump-server": "^1.7",
@@ -32,7 +34,8 @@
3234
"sort-packages": true,
3335
"allow-plugins": {
3436
"composer/package-versions-deprecated": true,
35-
"pixelfear/composer-dist-plugin": true
37+
"pixelfear/composer-dist-plugin": true,
38+
"php-http/discovery": true
3639
}
3740
},
3841
"extra": {

config/statamic/search.php

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,42 @@
2626
'indexes' => [
2727

2828
'default' => [
29-
'driver' => 'local',
30-
'searchables' => ['collection:*', 'taxonomy:*'],
31-
'fields' => ['title'],
29+
'driver' => 'meilisearch',
30+
'searchables' => ['docs:*'],
31+
'fields' => [
32+
'title',
33+
'search_title',
34+
'content',
35+
'search_content',
36+
'additional_context',
37+
'hierarchy_lvl0',
38+
'hierarchy_lvl1',
39+
'url',
40+
],
41+
'settings' => [
42+
'rankingRules' => [
43+
'words',
44+
'typo',
45+
'proximity',
46+
'attribute',
47+
'exactness',
48+
'hierarchy_lvl0:asc',
49+
],
50+
'searchableAttributes' => [
51+
'title',
52+
'search_title',
53+
'content',
54+
'search_content',
55+
'additional_context',
56+
'hierarchy_lvl0',
57+
'hierarchy_lvl1',
58+
'url',
59+
],
60+
],
61+
'content_retriever' => App\Search\RequestContentRetriever::class,
62+
'document_transformers' => [
63+
App\Search\DocTransformer::class,
64+
],
3265
],
3366

3467
// 'blog' => [
@@ -62,7 +95,12 @@
6295
'secret' => env('ALGOLIA_SECRET', ''),
6396
],
6497
],
65-
98+
'meilisearch' => [
99+
'credentials' => [
100+
'url' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
101+
'secret' => env('MEILISEARCH_KEY', ''),
102+
],
103+
],
66104
],
67105

68106
/*
@@ -76,7 +114,7 @@
76114
*/
77115

78116
'defaults' => [
79-
'fields' => ['title']
80-
]
117+
'fields' => ['title'],
118+
],
81119

82120
];

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"dependencies": {
3131
"@docsearch/js": "^3.0.0-alpha.40",
3232
"alpinejs": "^3.2.4",
33-
"dayjs": "^1.10.7"
33+
"dayjs": "^1.10.7",
34+
"meilisearch-docsearch": "^0.7.0"
3435
}
3536
}

0 commit comments

Comments
 (0)