Un adaptateur pour utiliser l'impressionnante bibliothèque Instantsearch.js avec un serveur de recherche Typesense, afin de créer des interfaces de recherche riches.
Voici un exemple d'interface utilisateur que vous pouvez créer avec cet adaptateur : songs-search.typesense.org
Remarque : Si votre interface de recherche est construite sur un composant de saisie semi-automatique personnalisé ou est basée sur @algolia/autocomplete-js, vous n'avez pas besoin de cet adaptateur pour l'utiliser avec Typesense, car la bibliothèque typesense-js prend déjà en charge la récupération côté client. données provenant de n’importe quelle source de données asynchrone. En savoir plus ici.
Les bonnes gens d'Algolia ont créé et open source Instantsearch.js, qui est un ensemble de composants prêts à l'emploi que vous pouvez utiliser pour créer rapidement des expériences de recherche interactives.
Avec l'adaptateur de ce référentiel, vous pourrez utiliser Instantsearch (et ses cousins React, Vue et Angular) avec des données indexées dans un serveur de recherche Typesense.
Si vous n'avez jamais utilisé Instantsearch auparavant, nous vous recommandons de consulter leur guide de démarrage ici. Une fois que vous avez parcouru le guide, suivez les instructions ci-dessous pour brancher l'adaptateur Typesense sur Instantsearch.
Voici un guide sur la création d'une interface de recherche rapide avec Typesense et InstantSearch.js : https://typesense.org/docs/0.20.0/guide/search-ui-components.html
Voici une application de démonstration qui vous montre comment utiliser l'adaptateur : https://github.com/typesense/typesense-instantsearch-demo
$ npm install --save typesense-instantsearch-adapter @babel/runtime
ou
$ yarn add typesense-instantsearch-adapter @babel/runtime
ou, vous pouvez également inclure directement l'adaptateur via une balise de script dans votre code HTML :
< script src =" https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2/dist/typesense-instantsearch-adapter.min.js " > </ script >
<!-- You might want to pin the version of the adapter used if you don't want to always receive the latest minor version -->
Puisqu'il s'agit d'un adaptateur, il n'installera pas automatiquement la bibliothèque Instantsearch pour vous. Vous devez installer l'un des éléments suivants directement dans votre application :
Vous trouverez des informations sur la façon de démarrer avec chacune des bibliothèques ci-dessus dans leurs dépôts respectifs.
Nous vous recommandons également de consulter create-instantsearch-app pour créer votre interface utilisateur de recherche à partir d'un modèle de démarrage.
import instantsearch from "instantsearch.js" ;
import { searchBox , hits } from "instantsearch.js/es/widgets" ;
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter" ;
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "abcd" , // Be sure to use an API key that only allows search operations
nodes : [
{
host : "localhost" ,
path : "" , // Optional. Example: If you have your typesense mounted in localhost:8108/typesense, path should be equal to '/typesense'
port : "8108" ,
protocol : "http" ,
} ,
] ,
cacheSearchResultsForSeconds : 2 * 60 , // Cache search results from server. Defaults to 2 minutes. Set to 0 to disable caching.
} ,
// The following parameters are directly passed to Typesense's search API endpoint.
// So you can pass any parameters supported by the search endpoint below.
// query_by is required.
additionalSearchParameters : {
query_by : "name,description,categories" ,
} ,
} ) ;
const searchClient = typesenseInstantsearchAdapter . searchClient ;
const search = instantsearch ( {
searchClient ,
indexName : "products" ,
} ) ;
search . addWidgets ( [
searchBox ( {
container : "#searchbox" ,
} ) ,
hits ( {
container : "#hits" ,
templates : {
item : `
<div class="hit-name">
{{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}
</div>
` ,
} ,
} ) ,
] ) ;
search . start ( ) ;
Vous pouvez ajouter ici n’importe quel widget Instantsearch pris en charge par l’adaptateur.
Vous trouverez également un exemple fonctionnel dans test/support/testground. Pour l'exécuter, exécutez npm run testground
à partir du dossier racine du projet.
import React from "react" ;
import ReactDOM from "react-dom" ;
import { SearchBox } from "react-instantsearch-dom" ;
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter" ;
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "abcd" , // Be sure to use an API key that only allows search operations
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "" , // Optional. Example: If you have your typesense mounted in localhost:8108/typesense, path should be equal to '/typesense'
protocol : "http" ,
} ,
] ,
cacheSearchResultsForSeconds : 2 * 60 , // Cache search results from server. Defaults to 2 minutes. Set to 0 to disable caching.
} ,
// The following parameters are directly passed to Typesense's search API endpoint.
// So you can pass any parameters supported by the search endpoint below.
// query_by is required.
additionalSearchParameters : {
query_by : "name,description,categories" ,
} ,
} ) ;
const searchClient = typesenseInstantsearchAdapter . searchClient ;
const App = ( ) => (
< InstantSearch indexName = "products" searchClient = { searchClient } >
< SearchBox / >
< Hits / >
< / InstantSearch >
) ;
Vous pouvez ensuite ajouter ici n’importe lequel des widgets Instantsearch-React pris en charge par l’adaptateur.
Les instructions ci-dessus s'appliquent également à React Native.
App.vue :
< template >
< ais-instant-search :search-client = " searchClient " index-name = " products " >
< ais-search-box />
< ais-hits >
< div slot = " item " slot-scope = " { item } " >
< h2 >{{ item.name }}</ h2 >
</ div >
</ ais-hits >
</ ais-instant-search >
</ template >
< script >
import TypesenseInstantSearchAdapter from " typesense-instantsearch-adapter " ;
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ({
server : {
apiKey : " abcd " , // Be sure to use an API key that only allows search operations
nodes : [
{
host : " localhost " ,
path : " " , // Optional. Example: If you have your typesense mounted in localhost:8108/typesense, path should be equal to '/typesense'
port : " 8108 " ,
protocol : " http " ,
},
],
cacheSearchResultsForSeconds : 2 * 60 , // Cache search results from server. Defaults to 2 minutes. Set to 0 to disable caching.
},
// The following parameters are directly passed to Typesense's search API endpoint.
// So you can pass any parameters supported by the search endpoint below.
// query_by is required.
additionalSearchParameters : {
query_by : " name,description,categories " ,
},
});
const searchClient = typesenseInstantsearchAdapter . searchClient ;
export default {
data () {
return {
searchClient,
};
},
};
</ script >
Vous pouvez ensuite ajouter ici n’importe quel widget Instantsearch pris en charge par l’adaptateur.
// app.component.ts
import { Component } from "@angular/core" ;
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter" ;
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "abcd" , // Be sure to use an API key that only allows search operations
nodes : [
{
host : "localhost" ,
path : "" , // Optional. Example: If you have your typesense mounted in localhost:8108/typesense, path should be equal to '/typesense'
port : "8108" ,
protocol : "http" ,
} ,
] ,
cacheSearchResultsForSeconds : 2 * 60 , // Cache search results from server. Defaults to 2 minutes. Set to 0 to disable caching.
} ,
// The following parameters are directly passed to Typesense's search API endpoint.
// So you can pass any parameters supported by the search endpoint below.
// query_by is required.
additionalSearchParameters : {
query_by : "name,description,categories" ,
} ,
} ) ;
const searchClient = typesenseInstantsearchAdapter . searchClient ;
@ Component ( {
selector : "app-root" ,
templateUrl : "./app.component.html" ,
styleUrls : [ "./app.component.css" ] ,
} )
export class AppComponent {
config = {
indexName : "products" ,
searchClient ,
} ;
}
Vous pouvez ensuite ajouter ici n’importe quel widget Instantsearch pris en charge par l’adaptateur.
hierarchicalMenu
Pour ce widget, vous souhaitez créer des champs indépendants dans le schéma de la collection avec cette convention de dénomination spécifique :
field.lvl0
field.lvl1
field.lvl2
pour une hiérarchie imbriquée de field.lvl0 > field.lvl1 > field.lvl2
Chacun de ces champs peut également contenir un tableau de valeurs. Ceci est utile pour gérer plusieurs hiérarchies.
sortBy
Lors de l'instanciation de ce widget, vous souhaitez définir la valeur du nom de l'index dans ce format particulier :
search . addWidgets ( [
sortBy ( {
container : "#sort-by" ,
items : [
{ label : "Default" , value : "products" } ,
{ label : "Price (asc)" , value : "products/sort/price:asc" } ,
{ label : "Price (desc)" , value : "products/sort/price:desc" } ,
] ,
} ) ,
] ) ;
Le modèle généralisé pour l'attribut value est : <index_name>[/sort/<sort_by>]
. L'adaptateur utilisera la valeur de <sort_by>
comme valeur du paramètre de recherche sort_by
.
configure
Si vous devez spécifier un paramètre de recherche filter_by
pour Typesense, vous souhaitez utiliser le widget configure
InstantSearch, ainsi que facetFilters
, numericFilters
ou filters
.
Le format des facetFilters
et numericFilters
est le même que celui d'Algolia, tel que décrit ici. Mais filters
doivent être au format filter_by
de Typesense, comme décrit dans ce tableau ici.
La définition filter_by
dans la configuration additionalQueryParameters
ne fonctionne que lorsque les widgets sont chargés initialement, car InstantSearch remplace en interne le champ filter_by
par la suite. En savoir plus ici.
index
Pour la recherche fédérée/multi-index, vous devrez utiliser le widget index
. Pour pouvoir ensuite spécifier différents paramètres de recherche pour chaque index/collection, vous pouvez les spécifier à l'aide de la configuration collectionSpecificSearchParameters
:
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "abcd" , // Be sure to use an API key that only allows search operations
nodes : [ { host : "localhost" , path : "/" , port : "8108" , protocol : "http" } ] ,
} ,
// Search parameters that are common to all collections/indices go here:
additionalSearchParameters : {
numTypos : 3 ,
} ,
// Search parameters that need to be *overridden* on a per-collection-basis go here:
collectionSpecificSearchParameters : {
products : {
query_by : "name,description,categories" ,
} ,
brands : {
query_by : "name" ,
} ,
} ,
} ) ;
const searchClient = typesenseInstantsearchAdapter . searchClient ;
Essentiellement, tous les paramètres définis dans collectionSpecificSearchParameters
seront fusionnés avec les valeurs dans additionalSearchParameters
lors de l'interrogation de Typesense, remplaçant ainsi les valeurs dans additionalSearchParameters
pour chaque collection.
geoSearch
Algolia utilise _geoloc
par défaut pour le nom du champ qui stocke les valeurs lat longues d'un enregistrement. Dans Typesense, vous pouvez nommer n’importe quel nom au champ de géolocalisation. Si vous utilisez un nom autre que _geoloc
, vous devez le spécifier lors de l'initialisation de l'adaptateur comme ci-dessous, afin qu'InstantSearch puisse y accéder :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
geoLocationField : "lat_lng_field" , // <<======
additionalSearchParameters ,
} ) ;
dynamicWidgets
Disponible à partir de Typesense Server
v0.25.0.rc12
Ce widget dynamicWidgets
fonctionne immédiatement sans modifications supplémentaires, mais si vous souhaitez contrôler l'ordre dans lequel ces facettes sont affichées dans l'interface utilisateur, Instantsearch s'attend à ce qu'un paramètre appelé renderingContent
soit défini.
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
renderingContent : {
// <<===== Add this, only if you want to control the order of the widgets displayed by dynamicWidgets
facetOrdering : {
facets : {
order : [ "size" , "brand" ] , // <<===== Change this as needed
} ,
} ,
} ,
additionalSearchParameters ,
} ) ;
En savoir plus sur toutes les options disponibles pour renderingContent
dans la documentation d'Algolia ici.
Disponible à partir de typesense-instantsearch-adapter
2.7.0-2
Si des champs de chaîne dans vos documents ont deux points :
dans leurs valeurs (par exemple, disons qu'il y a un champ appelé { brand: "a:b" }
, alors vous devrez ajouter un paramètre comme ci-dessous lors de l'instanciation de l'adaptateur :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
facetableFieldsWithSpecialCharacters : [ "brand" ] , // <======= Add string fields that have colons in their values here, to aid in parsing
additionalSearchParameters ,
} ) ;
Si des noms de champs numériques dans vos documents comportent des caractères spéciaux tels que >
, <
, =
(par exemple, disons qu'il y a un champ appelé { price>discount: 3.0 }
), vous devrez alors ajouter un paramètre comme ci-dessous lors de l'instanciation de l'adaptateur :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
facetableFieldsWithSpecialCharacters : [ "price>discount" ] , // // <======= Add numeric fields that have >, < or = in their names, to aid in parsing
additionalSearchParameters ,
} ) ;
facet_by
Disponible à partir de typesense-instantsearch-adapter
2.8.0-1
et Typesense Serverv0.26.0.rc25
Le paramètre facet_by
est géré par InstantSearch en interne lorsque vous utilisez les différents widgets de filtrage.
Mais si vous devez transmettre des options personnalisées au paramètre facet_by
(par exemple : options de tri côté serveur), vous pouvez utiliser le paramètre facetByOptions
comme indiqué ci-dessous :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
facetByOptions : {
brand : "(sort_by: _alpha:asc)" ,
category : "(sort_by: _alpha:desc)" ,
} , // <======= Add any facet_by parameter as a key value pair. Don't forget the surrounding parantheses in the value.
collectionSpecificFacetByOptions : {
collection1 : {
brand : "(sort_by: _alpha:desc)" ,
} ,
} , // <======= Use this parameter if multiple collections share the same field names, and you want to use different options for each field. This will override facetByOptions for that particular collection.
additionalSearchParameters ,
} ) ;
Notez que pour le tri dans RefinementLists, en plus du tri côté serveur Typesense, vous devez également transmettre le paramètre sortBy
au widget RefinementList pour trier également les résultats de manière appropriée côté client.
filter_by
Disponible à partir de typesense-instantsearch-adapter
2.8.0-5
Le paramètre filter_by
est géré par InstantSearch en interne lorsque vous utilisez les différents widgets de filtrage.
Par défaut, l'adaptateur utilise un filtrage exact ( filter_by: field:=value
) lors de l'envoi des requêtes à Typesense. Si vous devez configurer l'adaptateur pour qu'il utilise :
(filtrage non exact au niveau des mots - filter_by: field:value
), vous souhaitez instancier l'adaptateur à l'aide de la configuration filterByOptions
:
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
filterByOptions : {
brand : { exactMatch : false } , // <========== Add this to do non-exact word-level filtering
category : { exactMatch : false } ,
} ,
collectionSpecificFilterByOptions : {
collection1 : {
brand : { exactMatch : false } ,
} ,
} , // <======= Use this parameter if multiple collections share the same field names, and you want to use different options for each field. This will override filterByOptions for that particular collection.
additionalSearchParameters ,
} ) ;
Disponible à partir de typesense-instantsearch-adapter
2.9.0-0
Voici un moyen de désactiver les règles de remplacement/de conservation lorsque les utilisateurs sélectionnent un ordre de tri particulier :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
sortByOptions : {
"field1:desc,field2:desc" : { enable_overrides : false } , // <========== Add this to disable sorting when this particular Typesense `sort_by` string is generated by the sortBy widget
} ,
collectionSpecificSortByOptions : {
collection2 : {
"field1:desc,field2:desc" : { enable_overrides : false } ,
} ,
} , // <======= Use this parameter if multiple collections share the same field names, and you want to use different options for each field. This will override sortByOptions for that particular collection.
additionalSearchParameters ,
} ) ;
Si vous avez un widget sortBy configuré avec une valeur indexName de products/sort/price:asc
par exemple, alors la clé à l'intérieur sortByOptions
doit être price:asc
.
Disponible à partir de typesense-instantsearch-adapter
2.7.1-4
Par défaut, lorsque group_by
est utilisé comme paramètre de recherche, l'adaptateur aplatit les résultats de tous les groupes en une seule liste d'accès séquentiels.
Si vous souhaitez conserver les groupes, vous souhaitez définir flattenGroupedHits: false
lors de l'instanciation de l'adaptateur.
Cela placera le premier hit d'un groupe comme hit principal, puis ajoutera tous les hits du groupe dans une clé _grouped_hits
à l'intérieur de chaque hit.
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
flattenGroupedHits : false , // <=======
additionalSearchParameters ,
} ) ;
Disponible à partir de typesense-instantsearch-adapter
2.7.0-3
L'idée générale est d'abord de se connecter au cycle de vie des requêtes d'Instantsearch, d'intercepter la requête saisie et de l'envoyer à une API d'intégration, de récupérer les intégrations, puis d'envoyer les vecteurs à Typesense pour effectuer une recherche de vecteurs voisins les plus proches.
Voici une démo que vous pouvez exécuter localement pour voir cela en action : https://github.com/typesense/showcase-hn-comments-semantic-search.
Voici comment procéder dans Instantsearch.js :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "xyz" ,
nodes : [
{
host : "localhost" ,
port : "8108" ,
path : "/" ,
protocol : "http" ,
} ,
] ,
} ,
additionalSearchParameters ,
} ) ;
// from https://github.com/typesense/showcase-hn-comments-semantic-search/blob/8a33006cae58b425c53f56a64e1273e808cd9375/src/js/index.js#L101
const searchClient = typesenseInstantsearchAdapter . searchClient ;
search = instantsearch ( {
searchClient ,
indexName : INDEX_NAME ,
routing : true ,
async searchFunction ( helper ) {
// This fetches 200 (nearest neighbor) results for semantic / hybrid search
let query = helper . getQuery ( ) . query ;
const page = helper . getPage ( ) ; // Retrieve the current page
if ( query !== "" && [ "semantic" , "hybrid" ] . includes ( $ ( "#search-type-select" ) . val ( ) ) ) {
console . log ( helper . getQuery ( ) . query ) ;
helper
. setQueryParameter (
"typesenseVectorQuery" , // <=== Special parameter that only works in [email protected] and above
`embedding:([], k:200)` ,
)
. setPage ( page )
. search ( ) ;
console . log ( helper . getQuery ( ) . query ) ;
} else {
helper . setQueryParameter ( "typesenseVectorQuery" , null ) . setPage ( page ) . search ( ) ;
}
} ,
} ) ;
Il existe deux modes de mise en cache :
Mise en cache côté serveur :
Pour activer la mise en cache côté serveur, ajoutez un paramètre appelé useServerSideSearchCache: true
dans le bloc de configuration du server
de typesense-instantsearch-adapter comme ceci :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "..." ,
nearestNode : { ... } ,
nodes : [ ... ] ,
useServerSideSearchCache : true // <<< Add this to send use_cache as a query parameter instead of post body parameter
} ,
additionalSearchParameters : { ... }
} ) ;
Cela amènera l'adaptateur à ajouter ?use_cache=true
comme paramètre de requête d'URL à toutes les demandes de recherche initiées par l'adaptateur, ce qui obligera ensuite Typesense Server à activer la mise en cache côté serveur pour ces demandes.
Mise en cache côté client :
L'adaptateur dispose également d'une mise en cache côté client activée par défaut, pour éviter les appels réseau inutiles vers le serveur. La durée de vie de ce cache côté client peut être configurée comme ceci :
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter ( {
server : {
apiKey : "..." ,
nearestNode : { ... } ,
nodes : [ ... ] ,
cacheSearchResultsForSeconds : 2 * 60 // <<< Add this to configure the TTL for client-side cache in the browser
} ,
additionalSearchParameters : { ... }
} ) ;
Serveur Typesense | typesense-instantsearch-adaptateur | instantsearch.js | réagir-instantsearch | vue-instantsearch | recherche angulaire instantanée |
---|---|---|---|---|---|
>=v0.25.0 | >= v2.7.1 | >= 4,51 | >= 6,39 | >= 4,8 | >= 4,4 |
>= v0.25.0.rc14 | >= v2.7.0-1 | >= 4,51 | >= 6,39 | >= 4,8 | >= 4,4 |
>= v0.25.0.rc12 | >= v2.6.0 | >= 4,51 | >= 6,39 | >= 4,8 | >= 4,4 |
>=v0.24 | >=v2.5.0 | >= 4.2.0 | >= 6.0.0 | >= 2.2.1 | >= 3.0.0 |
>=v0.21 | >= v2.0.0 | >= 4.2.0 | >= 6.0.0 | >= 2.2.1 | >= 3.0.0 |
>= v0.19 | >= v1.0.0 | >= 4.2.0 | >= 6.0.0 | >= 2.2.1 | >= 3.0.0 |
>=v0.15 | >=v0.3.0 | >= 4.2.0 | >= 6.0.0 | >= 2.2.1 | >= 3.0.0 |
>= v0.14 | >=v0.2.0 | >= 4.2.0 | >= 6.0.0 | >= 2.2.1 | >= 3.0.0 |
>= v0.13 | >= v0.1.0 | >= 4.2.0 | >= 6.0.0 | >= 2.2.1 | >= 3.0.0 |
>= v0.12 | >=v0.0.4 | >= 4.2.0 | >= 6.0.0 | >= 2.2.1 | >= 3.0.0 |
Si une version particulière des bibliothèques ci-dessus ne fonctionne pas avec l'adaptateur, veuillez ouvrir un problème Github avec des détails.
Cet adaptateur fonctionne avec tous les widgets de cette liste
$ npm install
$ npm run typesenseServer
$ FORCE_REINDEX=true npm run indexTestData
$ npm link typesense-instantsearch-adapter
$ npm run testground
$ npm test
Pour sortir une nouvelle version, nous utilisons le package np :
$ npm install --global np
$ np
# Follow instructions that np shows you
Si vous avez des questions ou rencontrez des problèmes, veuillez créer un problème Github et nous ferons de notre mieux pour vous aider.
© 2020-présent Typesense, Inc.