Skip to content

Commit c3a7e24

Browse files
committed
Add the documentation for the search feature
1 parent a8e8dde commit c3a7e24

5 files changed

Lines changed: 397 additions & 0 deletions

File tree

docs/php/api/search.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Search
2+
3+
The search system allows users to find content across different types of objects through the full-text search.
4+
To make your objects searchable, you need to implement a search provider, a search result object and register an object type.
5+
6+
## Search Provider
7+
8+
The search provider defines how objects are queried, which database table and columns to use, and how access permissions are checked.
9+
Create a class that extends `wcf\system\search\AbstractSearchProvider`:
10+
11+
{jinja{ codebox(
12+
title="files/lib/system/search/FooSearch.class.php",
13+
language="php",
14+
filepath="php/api/search/FooSearch.class.php"
15+
) }}
16+
17+
### Required Methods
18+
19+
The following methods must be implemented in your search provider:
20+
21+
#### `cacheObjects(array $objectIDs, ?array $additionalData = null): void`
22+
23+
Bulk-loads all search result objects for the given IDs.
24+
This is called once with all matching IDs before `getObject()` is called for individual results.
25+
26+
#### `getObject(int $objectID): ?ISearchResultObject`
27+
28+
Returns a single cached search result object by its ID, or `null` if the object was not found.
29+
30+
#### `getTableName(): string`
31+
32+
Returns the database table name that contains the searchable content.
33+
34+
#### `getIDFieldName(): string`
35+
36+
Returns the fully qualified column name of the primary key (e.g. `wcf1_foo.fooID`).
37+
38+
### Optional Methods
39+
40+
The `AbstractSearchProvider` base class provides default implementations for these methods.
41+
Override them as needed.
42+
43+
#### `getSubjectFieldName(): string`
44+
45+
Returns the fully qualified column name of the subject/title field.
46+
Defaults to `{tableName}.subject`.
47+
48+
#### `getUsernameFieldName(): string`
49+
50+
Returns the fully qualified column name of the author's username.
51+
Defaults to `{tableName}.username`.
52+
53+
#### `getTimeFieldName(): string`
54+
55+
Returns the fully qualified column name of the creation timestamp.
56+
Defaults to `{tableName}.time`.
57+
58+
#### `getConditionBuilder(array $parameters): ?PreparedStatementConditionBuilder`
59+
60+
Returns additional SQL conditions to filter the search results, for example to enforce access permissions or only return published content.
61+
The `$parameters` array contains the provider-specific form parameters submitted by the user.
62+
Return `null` if no additional conditions are needed.
63+
64+
#### `getJoins(): string`
65+
66+
Returns additional SQL `JOIN` clauses needed for the search query.
67+
This is useful when the searchable content and its metadata (such as the author or timestamp) are stored in separate tables.
68+
69+
For example, the article system searches the `wcf1_article_content` table but needs to join `wcf1_article` for the author and timestamp:
70+
71+
```php
72+
public function getJoins(): string
73+
{
74+
return '
75+
INNER JOIN wcf1_article
76+
ON wcf1_article.articleID = ' . $this->getTableName() . '.articleID';
77+
}
78+
```
79+
80+
#### `isAccessible(): bool`
81+
82+
Returns whether the current user is allowed to use this search provider.
83+
Defaults to `true`.
84+
85+
#### `getFormTemplateName(): string`
86+
87+
Returns the name of a template that provides additional form fields for this search provider on the search page, for example a category selector.
88+
Return an empty string (the default) if no additional form fields are needed.
89+
90+
#### `assignVariables(): void`
91+
92+
Assigns template variables required by the form template returned by `getFormTemplateName()`.
93+
94+
#### `getAdditionalData(): ?array`
95+
96+
Returns additional data that should be stored with the search result and passed back to `getConditionBuilder()` when a cached search is revisited.
97+
98+
#### `getCustomIconName(): ?string`
99+
100+
Returns a custom icon name to display in the search results list.
101+
Return `null` (the default) to use the default icon.
102+
103+
## Search Result Object
104+
105+
The search result object wraps your data model and provides the data needed to render a search result entry.
106+
Create a decorator class that extends `wcf\data\DatabaseObjectDecorator` and implements `wcf\data\search\ISearchResultObject`:
107+
108+
{jinja{ codebox(
109+
title="files/lib/data/foo/SearchResultFoo.class.php",
110+
language="php",
111+
filepath="php/api/search/SearchResultFoo.class.php"
112+
) }}
113+
114+
The `ISearchResultObject` interface requires the following methods:
115+
116+
| Method | Return Type | Description |
117+
| ------ | ----------- | ----------- |
118+
| `getUserProfile()` | `?UserProfile` | Returns the author's user profile. |
119+
| `getSubject()` | `string` | Returns the title/subject of the object. |
120+
| `getTime()` | `int` | Returns the creation timestamp. |
121+
| `getLink($query = '')` | `string` | Returns the URL to the object. When `$query` is set, it should be included as a `highlight` parameter. |
122+
| `getObjectTypeName()` | `string` | Returns the object type name (e.g. `com.example.foo`). |
123+
| `getFormattedMessage()` | `string` | Returns the message text formatted for display in search results. Use `SearchResultTextParser` to truncate and highlight the text. |
124+
| `getContainerTitle()` | `string` | Returns the title of the object's container (e.g. a category or forum). Return an empty string if there is no container. |
125+
| `getContainerLink()` | `string` | Returns the URL of the object's container. Return an empty string if there is no container. |
126+
127+
You also need a corresponding list class that sets `SearchResultFoo` as the decorator:
128+
129+
{jinja{ codebox(
130+
title="files/lib/data/foo/SearchResultFooList.class.php",
131+
language="php",
132+
filepath="php/api/search/SearchResultFooList.class.php"
133+
) }}
134+
135+
## Object Type Registration
136+
137+
Register your search provider as an object type with the definition `com.woltlab.wcf.searchableObjectType`:
138+
139+
```xml
140+
<type>
141+
<name>com.example.foo</name>
142+
<definitionname>com.woltlab.wcf.searchableObjectType</definitionname>
143+
<classname>wcf\system\search\FooSearch</classname>
144+
<searchindex>wcf1_foo_search_index</searchindex>
145+
</type>
146+
```
147+
148+
The `searchindex` element specifies the name of the search index table.
149+
If omitted, the system will automatically generate a table name based on the object type name.
150+
151+
## Language Item
152+
153+
You need to create a language item for the search type label that is displayed in the search form.
154+
The language item follows the pattern `wcf.search.type.{objectTypeName}`:
155+
156+
```
157+
wcf.search.type.com.example.foo = Foo
158+
```
159+
160+
## Managing the Search Index
161+
162+
### Adding and Updating Entries
163+
164+
Whenever searchable content is created or updated, you must update the search index using `SearchIndexManager::getInstance()->set()`:
165+
166+
```php
167+
use wcf\system\search\SearchIndexManager;
168+
169+
SearchIndexManager::getInstance()->set(
170+
'com.example.foo', // object type name
171+
$foo->fooID, // object ID
172+
$foo->message, // message text (HTML)
173+
$foo->title, // subject
174+
$foo->time, // timestamp
175+
$foo->userID, // author's user ID
176+
$foo->username, // author's username
177+
$foo->languageID, // language ID (or null)
178+
$foo->teaser // optional: metadata (e.g. teaser text)
179+
);
180+
```
181+
182+
The `$message` parameter accepts HTML content.
183+
The `SearchIndexManager` will automatically strip HTML tags before indexing.
184+
185+
### Deleting Entries
186+
187+
When objects are deleted, remove them from the search index:
188+
189+
```php
190+
SearchIndexManager::getInstance()->delete('com.example.foo', [$foo->fooID]);
191+
```
192+
193+
### Rebuilding the Index
194+
195+
If your content type has a rebuild data worker, reset and rebuild the search index in its `execute()` method:
196+
197+
```php
198+
use wcf\system\search\SearchIndexManager;
199+
200+
// Reset the search index on the first iteration.
201+
if (!$this->loopCount) {
202+
SearchIndexManager::getInstance()->reset('com.example.foo');
203+
}
204+
205+
// Re-index each object.
206+
foreach ($this->objectList as $foo) {
207+
SearchIndexManager::getInstance()->set(
208+
'com.example.foo',
209+
$foo->fooID,
210+
$foo->message,
211+
$foo->title,
212+
$foo->time,
213+
$foo->userID,
214+
$foo->username,
215+
$foo->languageID
216+
);
217+
}
218+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ nav:
6969
- 'User Activity Points': 'php/api/user_activity_points.md'
7070
- 'User Notifications': 'php/api/user_notifications.md'
7171
- 'RSS Feeds': 'php/api/rss_feeds.md'
72+
- 'Search': 'php/api/search.md'
7273
- 'Sitemaps': 'php/api/sitemaps.md'
7374
- 'Code Style': 'php/code-style.md'
7475
- 'Apps': 'php/apps.md'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace wcf\system\search;
4+
5+
use wcf\data\foo\SearchResultFoo;
6+
use wcf\data\foo\SearchResultFooList;
7+
use wcf\data\search\ISearchResultObject;
8+
use wcf\system\database\util\PreparedStatementConditionBuilder;
9+
use wcf\system\WCF;
10+
11+
/**
12+
* Search provider for foo objects.
13+
*/
14+
class FooSearch extends AbstractSearchProvider
15+
{
16+
/**
17+
* @var SearchResultFoo[]
18+
*/
19+
private array $messageCache = [];
20+
21+
#[\Override]
22+
public function cacheObjects(array $objectIDs, ?array $additionalData = null): void
23+
{
24+
$list = new SearchResultFooList();
25+
$list->setObjectIDs($objectIDs);
26+
$list->readObjects();
27+
foreach ($list->getObjects() as $foo) {
28+
$this->messageCache[$foo->fooID] = $foo;
29+
}
30+
}
31+
32+
#[\Override]
33+
public function getObject(int $objectID): ?ISearchResultObject
34+
{
35+
return $this->messageCache[$objectID] ?? null;
36+
}
37+
38+
#[\Override]
39+
public function getTableName(): string
40+
{
41+
return 'wcf1_foo';
42+
}
43+
44+
#[\Override]
45+
public function getIDFieldName(): string
46+
{
47+
return $this->getTableName() . '.fooID';
48+
}
49+
50+
#[\Override]
51+
public function getSubjectFieldName(): string
52+
{
53+
return $this->getTableName() . '.title';
54+
}
55+
56+
#[\Override]
57+
public function getUsernameFieldName(): string
58+
{
59+
return $this->getTableName() . '.username';
60+
}
61+
62+
#[\Override]
63+
public function getTimeFieldName(): string
64+
{
65+
return $this->getTableName() . '.time';
66+
}
67+
68+
#[\Override]
69+
public function getConditionBuilder(array $parameters): ?PreparedStatementConditionBuilder
70+
{
71+
$conditionBuilder = new PreparedStatementConditionBuilder();
72+
73+
// Only show foo objects that the current user can access.
74+
if (!WCF::getSession()->getPermission('mod.foo.canViewAll')) {
75+
$conditionBuilder->add('wcf1_foo.isPublished = ?', [1]);
76+
}
77+
78+
return $conditionBuilder;
79+
}
80+
81+
#[\Override]
82+
public function isAccessible(): bool
83+
{
84+
return WCF::getSession()->getPermission('user.foo.canSearch');
85+
}
86+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace wcf\data\foo;
4+
5+
use wcf\data\DatabaseObjectDecorator;
6+
use wcf\data\search\ISearchResultObject;
7+
use wcf\data\user\UserProfile;
8+
use wcf\system\request\LinkHandler;
9+
use wcf\system\search\SearchResultTextParser;
10+
11+
/**
12+
* Represents a foo object as a search result.
13+
*
14+
* @mixin Foo
15+
* @extends DatabaseObjectDecorator<Foo>
16+
*/
17+
class SearchResultFoo extends DatabaseObjectDecorator implements ISearchResultObject
18+
{
19+
protected static $baseClass = Foo::class;
20+
21+
#[\Override]
22+
public function getUserProfile(): ?UserProfile
23+
{
24+
return $this->getDecoratedObject()->getUserProfile();
25+
}
26+
27+
#[\Override]
28+
public function getSubject(): string
29+
{
30+
return $this->getDecoratedObject()->getTitle();
31+
}
32+
33+
#[\Override]
34+
public function getTime(): int
35+
{
36+
return $this->getDecoratedObject()->time;
37+
}
38+
39+
#[\Override]
40+
public function getLink($query = ''): string
41+
{
42+
$parameters = [
43+
'object' => $this->getDecoratedObject(),
44+
'forceFrontend' => true,
45+
];
46+
47+
if ($query) {
48+
$parameters['highlight'] = \urlencode($query);
49+
}
50+
51+
return LinkHandler::getInstance()->getLink('Foo', $parameters);
52+
}
53+
54+
#[\Override]
55+
public function getObjectTypeName(): string
56+
{
57+
return 'com.example.foo';
58+
}
59+
60+
#[\Override]
61+
public function getFormattedMessage(): string
62+
{
63+
return SearchResultTextParser::getInstance()->parse(
64+
$this->getDecoratedObject()->getMessage()
65+
);
66+
}
67+
68+
#[\Override]
69+
public function getContainerTitle(): string
70+
{
71+
return '';
72+
}
73+
74+
#[\Override]
75+
public function getContainerLink(): string
76+
{
77+
return '';
78+
}
79+
}

0 commit comments

Comments
 (0)