Skip to main content

Hello Search

Hello Search is the simplest kind of Search-Based Application you can build with the SBA Framework. It is made of a single module and a single Angular component, which shows a search form and a list of results:

Hello Search

App module

Hello Search has one Angular module (AppModule) in src/app/app.module.ts. It looks very much like the default app.module.ts you would get from creating a new Angular app with ng new, with some specific points:

  • We import required modules from @sinequa/core and pass them configuration via their forRoot() methods.
  • In particular we must pass the StartConfig object to the WebServicesModule. This object contains the URL of the sinequa server (which can be omitted when the app is hosted on the server) and the name of the App configured in the Sinequa administration.
  • We use {provide: LocationStrategy, useClass: HashLocationStrategy}, to manage routes (which is not specific to Hello Search).

App component

Hello Search has one Angular component (AppComponent). It is made of:

Template

The template (src/app/app.component.html) is divided in four parts:

Search Form:

The search form has a search field <input>, and two buttons ("Search" and "Clear"). When submitting the form, the search() method from the controller is called. Note that the form is deactivated if the user is not logged in.

<form novalidate [formGroup]="form">
<input type="text" placeholder="Enter search terms..." formControlName="search" spellcheck="false" autocomplete="off" [attr.disabled]="!loginService.complete? '' : null">
<button type="submit" (click)="search()" [attr.disabled]="!loginService.complete? '' : null">Search</button>
<button *ngIf="results$ | async" type="button" (click)="clear()">Clear</button>
</form>

Results:

The results list displays the list of Record objects from a Results object provided by the controller. The first *ngIf and | async allow to display the results only when they become available (it is asynchronous, since the server cannot respond instantaneously). Then, the *ngFor iterates through the list of results. Inside this <div>, we then display the title, source and relevant extracts of each document.

<div *ngIf="results$ | async; let results">
<hr>
<div *ngFor="let record of results.records" class="record">
<a href="{{record.url1}}">
<h3 [innerHtml]="record.displayTitle || record.title"></h3>
</a>
<div class="source">{{record.url1}}</div>
<p *ngIf="record.relevantExtracts" [innerHTML]="record.relevantExtracts"></p>
</div>
</div>

Login/Logout buttons:

These buttons call the login() and logout() methods of the controller.

<button *ngIf="loginService.complete" type="button" (click)="logout()">Logout</button>
<button *ngIf="!loginService.complete" type="button" (click)="login()">Login</button>

Notifications:

Notifications are typically some error messages coming from the Sinequa services and managed by the NotificationModule from @sinequa/core. If you fail to log in or to get data from the Sinequa indexes, you will likely see a message displayed at the bottom of the app.

<ng-container *ngIf="notificationsService.notificationsStream | async as notification">
<hr>
<div *ngIf="deleteNotification(notification)" class="notification">
<div *ngIf="notification.title" class="title">
<span>{{notification.title | sqMessage}}</span>
<hr>
</div>
<div>{{notification.text | sqMessage:{values: notification.params} }}</div>
</div>
</ng-container>

Controller

The controller (src/app/app.component.ts) consists of the class AppComponent, which is made of the main following parts:

Fields:

The AppComponent class has the following fields:

  • searchControl: An Angular UntypedFormControl object used to handle the search input value.
  • form: An Angular UntypedFormGroup object needed to interact with the content of a <form>.
  • results$: An rxjs Observable of Results (since results are retrieved asynchronously) which can also be undefined.
searchControl: UntypedFormControl;
form: UntypedFormGroup;
results$: Observable<Results> | undefined;

Constructor:

In the constructor, we inject the following services from @sinequa/core (and initialize our form):

  • LoginService: Service in charge of authentication and initialization of other services.
  • AppService: Service in charge of retrieving the configuration of your application from the Sinequa server.
  • QueryWebService: Service in charge of sending queries and retrieving results from the Sinequa server.
  • NotificationsService: Service in charge of centralizing errors and warnings from other services.
constructor(
protected formBuilder: FormBuilder,
public loginService: LoginService,
public appService: AppService,
public queryWebService: QueryWebService,
public notificationsService: NotificationsService) {

this.searchControl = new UntypedFormControl("");
this.form = this.formBuilder.group({
search: this.searchControl
});
}

Search method:

The search() method is called when the user clicks on the "search" button. It performs the following tasks:

  • Create a new Query object (with the right name, retrieved from the app configuration.
  • Set the query.text to the value typed by the user in the search form (this.searchControl.value).
  • Send the query to the QueryWebService and get the results observable (results$). When results are available (asynchronously), the template will display them.
search() {
const ccquery = this.appService.ccquery;
const query = new Query(ccquery ? ccquery.name : "_unknown");
query.text = this.searchControl.value || "";
this.results$ = this.queryWebService.getResults(query);
}

Login and Logout method:

The login() and logout() methods are essentially proxies to the corresponding methods in the LoginService which manages the authentication. Note that the LoginService also takes care of retrieving data from the server via three services:

  • The AppWebService, which retrieves the configuration of the applications.
  • The PrincipalWebService, which retrieves the user data from its domain (it includes the name, email, id, and other data).
  • The UserSettingsWebService, which retrieves the User Settings (more information in the Tutorial and the Tips & Tricks)

Additionally, we clear the results on log out, by removing the results$ and emptying the search form.

clear() {
this.results$ = undefined;
this.searchControl.setValue("");
}

login() {
this.loginService.login();
}

logout() {
this.clear();
this.loginService.logout();
}

Stylesheet

The component's stylesheet (src/app/app.component.scss) contains CSS rules applied only within the component. In particular:

  • Page layout rules, making our search results more readable:

    .search {
    max-width: 800px;
    margin-left: 100px;
    }
  • Text sizing, coloring and spacing for the title (h1), document title (h3), document source (.source) and relevant extracts (p).

  • Styling of the notifications:

    .notification {
    border: solid;
    padding: 8px;

    .title {
    font-weight: bold;
    }
    }

In addition to the component's stylesheet, a global stylesheet (src/styles/app.scss) contains styles that apply to the whole app. This is where you would import (globally) third party styling libraries such as Bootstrap (see the tutorial).

@import "~@angular/cdk/overlay-prebuilt";

body {
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
}

a {
text-decoration: none;
color: #3434d6;
}

.record .match-highlight {
font-weight: bold;
font-style: italic;
}