Skip to content

Instantly share code, notes, and snippets.

@jsquire
Last active March 14, 2022 21:43
Show Gist options
  • Save jsquire/2bae19a028e9d87023e8e1bb611a136d to your computer and use it in GitHub Desktop.
Save jsquire/2bae19a028e9d87023e8e1bb611a136d to your computer and use it in GitHub Desktop.
Text Analytics: Client API Evolution Thoughts

Text Analytics: Client API Evolution Thoughts

Currently the TextAnalytics client offers a set of bespoke methods for each operation that developers may wish to perform. These methods are associated with the operation by name, intending to allow operations to be discoverable when browsing code completion lists and organizing related methods into groups.

This pattern has worked well with the Text Analytics REST API, which offered roughly seven core analysis skills. As Text Analytics moves to the Unified Language API, it is expected that the number of skills offered will grow steadily. This growth may cause the number of bespoke methods to become burdensome for developers, necessitating an evolution of the client API.

Things to know before reading

  • The names used in this document are intended for illustration only. The names for Text Analytics skills are placeholders to simulate volume and do not reflect the actual service API.

  • The intent of this document is to consider high-level patterns. No attempt was made to faithfully represent service operations or unrelated client members. Details not related to the high-level concept are not illustrated.

  • Fake methods are used to illustrate "something needs to happen, but the details are unimportant." As a general rule, if an operation is not directly related to one of the Text Analytics types, it can likely be assumed that it is for illustration only. These methods will most often use ellipses for the parameter list, in order to help differentiate them.

Why this is needed

Each analysis skill is represented in the client by multiple overloads and call pattern variants. Becuse of this, each skill becomes to six individual bespoke methods:

- Skill(string)
- SkillAsync(string)
- SkillBatch(string)
- SkillBatch(OperationDocumentType)
- SkillBatchAsync(string)
- SkillBatchAsync(OperationDocumentType)

As the number of service operations grow, the client surface will grow more quickly and likely be perceived as cluttered, potentially impeding discovery and degrading the developer experience.

API modeling assumptions and conventions

The following assumptions were used for ideation:

  • Developers using Text Analytics are aware of its capabilities and explire the client library by seeking to apply a specific analysis skill to their input. They are not using the client library to explore/understand the capabilities offered by the service.

  • There are 40 analysis skils available for Text Analytics

  • Each analyze skill has a unique input parameter and return type

  • The client needs support syncrhonous and asynchronous versions of:

    • Analyze (analyze a single input using a specific skill)
    • AnalyzeBatch (analyze a set of inputs using a set of skills)
    • StartAnalyzeBatch (start a job to analyze a set of inputs using a set of skills)

Additional context and details for the types demonstrated in the usage snippets for different approaches can be found in Addendum: Support types.

Option 1 (current recommendation): "Kitchen Sink" client

In this approach, the number of client method names are constrained by using overloads to represent each distinct skill. This allows for a straight-forward discovery of the service operations using code-completion and offers a consistent pattern for understanding parameters and return types.

Client creation and "Hello World" scenario

// Create the client.

var client = new KitchenSinkClient(new Uri("http://this.is.fake"), new AzureKeyCredential("definitely-not-real"));

// Demonstrate the Hello World scenario of performing a simple analysis on a single
// text document.  

ExtractEntitiesResult result = 
    client.Analyze(new ExtractEntitiesSkill(), new TextAnalyticsDocument("This is a document"));

Single document analysis with skill-specific options

var skill = new AnalyzeCategoriesSkill
{
    DisplayName = "Look!",
    AnalyzeCategoriesSetting = "dummy"
};

AnalyzeCategoriesResult result = 
    await client.AnalyzeAsync(skill, new TextAnalyticsDocument("Text"));

Advanced scenario: analyze multiple documents with multiple skills

var documents = new[]
{
    new TextAnalyticsDocument("Some text", "es-sp"),
    new TextAnalyticsDocument("More stuff")
};

var skills = new TextAnalyticsSkill[]
{
    new RecognizeCategoriesSkill(),
    new AnalyzeCategoriesSkill(),
    new ClassifyCategoriesSkill()
};

AnalyzeBatchOperation operation = await client.StartAnalyzeBatchAsync(documents, skills, new AnalyzeBatchOptions());

foreach (AnalyzeBatchResult batchResult in operation.GetValues())
{
    switch (batchResult.Skill)
    {
        case RecognizeCategoriesSkill:
            var recognizeResult = (RecognizeCategoriesResult)batchResult.Result;
            // Process the result
            break;

        case AnalyzeCategoriesSkill:
            var analyzeResult = (AnalyzeCategoriesResult)batchResult.Result;
            // Process the result
            break;

        case ClassifyCategoriesSkill:
            var classifyResult = (ClassifyCategoriesResult)batchResult.Result;
            // Process the result
            break;
            
        default:
            throw new InvalidOperationResult("Unknown result type");
    }
}

Benefits

  • Initial discovery of the available service operations is simplified, as the code-completion list is short and the methods clearly named.
    kitchen-sink-methods

  • New skills added to the service do not create additional code-completion clutter for initial discovery.

  • Skill use follows a consistent and predictable pattern; once the service operations are known, any skill can be applied without relearning patterns.

Challenges

  • There are large number of overloads for each method and the code-completion list is potentially intimidating; exploring the available skills from the list would be a poor experience.
    kitchen-sing-overloads

  • Batched operation support requires deeper knowledge of the skill/result pairings.

Option 2 (not recommended): "Polymporphic" client

In this approach, the cient mirrors the approach used by the service, exposing methods for each operation. To reduce overload explosion, method parameters and return types are distilled down to the common base class. This allows for straight-forward discovery of service operations using code-completion, though it makes understanding of usage more difficult.

Client creation and "Hello World" scenario

 // Create the client.

var client = new PolymorphicClient(new Uri("http://this.is.fake"), new AzureKeyCredential("definitely-not-real"));

// Demonstrate the Hello World scenario of performing a simple analysis on a single
// text document.

ExtractEntitiesResult result = 
    (ExtractEntitiesResult)client.Analyze(new ExtractEntitiesSkill(), new TextAnalyticsDocument("This is a document"));

Single document analysis with skill-specific options

var skill = new AnalyzeCategoriesSkill
{
    DisplayName = "Look!",
    AnalyzeCategoriesSetting = "dummy"
};

switch (await client.AnalyzeAsync(skill, new TextAnalyticsDocument("Text")))
{
    case AnalyzeCategoriesResult:
        // Do a thing
        break;
        
    case AnalyzeCategoriesCustomResult:
        // Do a custom thing
        break;
        
    default:
        throw new NotSupportedException("The result type is not supported.");
}

Advanced scenario: analyze multiple documents with multiple skills

var documents = new[]
{
    new TextAnalyticsDocument("Some text", "es-sp"),
    new TextAnalyticsDocument("More stuff")
};

var skills = new TextAnalyticsSkill[]
{
    new RecognizeCategoriesSkill(),
    new AnalyzeCategoriesSkill(),
    new ClassifyCategoriesSkill()
};

AnalyzeBatchOperation operation = await client.StartAnalyzeBatchAsync(documents, skills, new AnalyzeBatchOptions());

foreach (AnalyzeBatchResult batchResult in operation.GetValues())
{
    switch (batchResult.Skill)
    {
        case RecognizeCategoriesSkill:
            var recognizeResult = (RecognizeCategoriesResult)batchResult.Result;
            // Process the result
            break;

        case AnalyzeCategoriesSkill:
            var analyzeResult = (AnalyzeCategoriesResult)batchResult.Result;
            // Process the result
            break;

        case ClassifyCategoriesSkill:
            var classifyResult = (ClassifyCategoriesResult)batchResult.Result;
            // Process the result
            break;
            
        default:
            throw new InvalidOperationResult("Unknown result type");
    }
}

Benefits

  • Aligns with the structure of the REST API, providing familiarity to developers using it.

  • Reduces the number of items in code-completion, for both method discovery and the overload list.

Challenges

  • Requires deeper knowledge to use; developers must understand the available operation/result type pairings and the type hierarchy.

  • Exploration of the client in code-completion is much less effective; developers will have a higher reliance on documentation and samples.

  • Explicit casting is needed in most scenarios; this makes the code more dense, potentially more error-prone, and will have an impact on performance.

  • Batched operation support requires deeper knowledge of the skill/result pairings.

Option 3 (not recommended): "Subclient" client

In this approach, the top-level client serves as a factory for subclients that encapsulate the available service operations. This allows the top-level client surface to be scoped to cross-skill and factory operations with skill-specific operations being delegated to the subclients.

Client creation and "Hello World" scenario

 // Create the client.

var client = new SubclientClient(new Uri("http://this.is.fake"), new AzureKeyCredential("definitely-not-real"));

// Demonstrate the Hello World scenario of performing a simple analysis on a single
// text document.

ExtractEntitiesClient skillClient = client.GetExtractEntitiesClient();

ExtractEntitiesResult result = 
    skillClient.Analyze(new ExtractEntitiesSkill(), new TextAnalyticsDocument("This is a document"));

Single document analysis with skill-specific options

var skill = new AnalyzeCategoriesSkill
{
    DisplayName = "Look!",
    AnalyzeCategoriesSetting = "dummy"
};

AnalyzeCategoriesClient skillClient = client.GetAnalyzeCategoriesClient();

AnalyzeCategoriesResult result = 
    await skillClient.AnalyzeAsync(skill, new TextAnalyticsDocument("Text"));

Advanced scenario: analyze multiple documents with multiple skills

var documents = new[]
{
    new TextAnalyticsDocument("Some text", "es-sp"),
    new TextAnalyticsDocument("More stuff")
};

var skills = new TextAnalyticsSkill[]
{
    new RecognizeCategoriesSkill(),
    new AnalyzeCategoriesSkill(),
    new ClassifyCategoriesSkill()
};

AnalyzeBatchOperation operation = await client.StartAnalyzeBatchAsync(documents, skills, new AnalyzeBatchOptions());

foreach (AnalyzeBatchResult batchResult in operation.GetValues())
{
    switch (batchResult.Skill)
    {
        case RecognizeCategoriesSkill:
            var recognizeResult = (RecognizeCategoriesResult)batchResult.Result;
            // Process the result
            break;

        case AnalyzeCategoriesSkill:
            var analyzeResult = (AnalyzeCategoriesResult)batchResult.Result;
            // Process the result
            break;

        case ClassifyCategoriesSkill:
            var classifyResult = (ClassifyCategoriesResult)batchResult.Result;
            // Process the result
            break;
            
        default:
            throw new InvalidOperationResult("Unknown result type");
    }
}

Benefits

  • Reduces the number of items in code-completion for the top-level client, highlighting the available skills.

Challenges

  • There is no natural categorization to the Text Analytics skills; it isn't reasonable to group skills together and represent each group by a subclient. This le

  • Because the subclients are skill-focused, they have the potential to cause clutter on the top-level client and make discovery more difficult as the number of skills increases over time.

  • With each subclient being skill-focused, there is the potential for an explosion of public types as the number of skills available increases over time.

Future considerations

  • If it becomes possible to have an intuitive grouping of skills, there may be merit in revisiting the subclient approach. If skills can be segmented into a small number of groups, then organizing subclients into bespoke methods similar to the API surface for the v5.x packages may allow a familiar usage pattern and good discovery without suffering from explosion as the number of skills increases over time.

  • It may be worth considering moving cross-skill operations to another subclient type rather than representing them on the top-level client. This would allow the top-level client to have singular focus as a client factory.

Addendum: Support types

For the purposes of ideation and mocking, conventions were applied which are likely to loosely map to the real-world implementation. This addendum illustrates examples of these conventions in order to provide additional context to usage snippets.

Skills

Each available skill is represented by a concrete type, derrived from a common base and extended with any skill-specific attributes.

// Common base class
public abstract class TextAnalyticsSkill
{
    public string DisplayName { get; set; }
}

// Example skills
public class AnalyzeCategoriesSkill : TextAnalyticsSkill
{
    public string AnalyzeCategoriesSetting { get; set; }
}

public class ClassifySentimentSkill : TextAnalyticsSkill
{
    public double MinimumConfidenceLevel { get; set; }
}

Results

Each Analyze operation result is represented by a concrete type, derrived from a common base and extended with any skill-specific attributes. Results have a 1:1 correspondance with skills; it is not possible to have a skill type without a matching result type.

// Common base class
public abstract class TextAnalyticsResult
{
}

// Example results
public class AnalyzeCategoriesResult : TextAnalyticsResult
{
    public IReadOnlyList<string> Categories { get; internal set; }
}

public class ClassifySentimentResult : TextAnalyticsResult
{
    public double Confidence { get; internal set; }
    public string Value { get; internal set; }
}

Input types

Each skill is applied to a document, which is a general type that can be used with all skills. It contains attributes and information about what is being analyzed; any settings for how to analyze are represented by the skill.

public class TextAnalyticsDocument
{
    public string Language { get; set; } = "en/us";
    public string Content { get; set; }
    public string Name { get; set; }
}

Skill-based subclient types

The available operations for each skill are represented by a skill-specific subclient, created by the main client. Each follows a common pattern but may be customized with skill-specific helpers, if needed.

public class AnalyzeCategoriesClient
{
    internal AnalyzeCategoriesClient() {}
    public Task<AnalyzeCategoriesResult> AnalyzeAsync(AnalyzeCategoriesSkill skill, TextAnalyticsDocument document, CancellationToken cancellationToken = default);
    public Task<AnalyzeCategoriesResult> AnalyzeAsync(AnalyzeCategoriesSkill skill, IEnumerable<TextAnalyticsDocument> documents, CancellationToken cancellationToken = default);
    public AnalyzeCategoriesResult Analyze(AnalyzeCategoriesSkill skill, TextAnalyticsDocument document, CancellationToken cancellationToken = default);
    public AnalyzeCategoriesResult Analyze(AnalyzeCategoriesSkill skill, IEnumerable<TextAnalyticsDocument> documents, CancellationToken cancellationToken = default);
}

public class DetectPhrasesClient
{
    internal DetectPhrasesClient() {}
    public Task<DetectPhrasesResult> AnalyzeAsync(DetectPhrasesSkill skill, TextAnalyticsDocument document, CancellationToken cancellationToken = default);
    public Task<DetectPhrasesResult> AnalyzeAsync(DetectPhrasesSkill skill, IEnumerable<TextAnalyticsDocument> documents, CancellationToken cancellationToken = default);
    public DetectPhrasesResult Analyze(DetectPhrasesSkill skill, TextAnalyticsDocument document, CancellationToken cancellationToken = default);
    public DetectPhrasesResult Analyze(DetectPhrasesSkill skill, IEnumerable<TextAnalyticsDocument> documents, CancellationToken cancellationToken = default);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment